Session problem in PHP – trying to create a simple CAPTCHA

The definitive solution with session is.. do not use session.

As example in your form, you can output 3 fields:

  • an image field -> the captcha image
  • a text field -> to allow users to insert the characters
  • an hidden field -> contain the solution but encripted using wp_create_nonce

In this way when the form is submitted you can compare the user input with the hidden field input using wp_verify_nonce and you solve your problem without using session.

I’ll write the code in a class, you can put all this code in a file, name it 'mycaptcha.php' and save it in the theme folder, after that, require_once it from functions.php.

<?php
class MyCaptcha {
  protected $code="";
  protected $letters="23456789bcdfghjkmnpqrstvwxyz";
  protected $chars_num;
  protected $tcolor;
  protected $ncolor;
  protected $width;
  protected $height;

  /**
   * Allow to configure some options, generate a random code and print the fields
   */
  function __construct( $args = array() ) {
    $defaults = array(
      'chars_num' => 6,
      'dots' => 0,
      'lines' => 20,
      'width' => 110,
      'height' => 35,
      'tcolor' => 'B9B098',
      'ncolor' => 'CEC7BA'
    );
    $args = wp_parse_args( $args, $defaults );
    foreach ( $args as $k => $v ) {
      $this->$k = $v;
    }
    $this->setCode();
    $this->fields();
  }

  /**
   * Print the fields
   */
  protected function fields() {
    echo $this->getHidden();
    echo $this->getImg();
    echo $this->getText();
  }

  /**
   * Generate a random code
   */    
  protected function setCode() {
    $i = 0;
    $len = strlen( $this->letters ) - 1;
    while ( $i < $this->chars_num ) { 
      $this->code .= substr( $this->letters, mt_rand( 0, $len ), 1 );
      $i++;
    }
  }

  /**
   * Print hidden field
   */ 
  protected function getHidden() {
    return wp_nonce_field( $this->code , md5(__CLASS__) . '_n', FALSE, FALSE );
  }

  /**
   * Print text field
   */     
  protected function getText() {
    $f="<label>%s<input type="text" name="https://wordpress.stackexchange.com/questions/139907/%s" value="" autocomplete="off" /></label>";
    return sprintf( $f, __('Type the captcha:', 'your-textdomain'),  md5(__CLASS__) );
  }

  /**
   * Print captcha image
   */      
  protected function getImg() {
    $f="<img src="https://wordpress.stackexchange.com/questions/139907/%s" width="%d" height="%d" alt="" />";
    $data = array(
      'c' => self::encode( $this->code ),
      'size' => "{$this->width}x{$this->height}",
      'dots' => "{$this->dots}",
      'lines' => "{$this->lines}",
      'tcolor' => "{$this->tcolor}",
      'ncolor' => "{$this->ncolor}"
    );
    $url = add_query_arg( $data, get_template_directory_uri() . '/captcha_img.php' );
    return sprintf( $f, $url, $this->width, $this->height );
  }

  /**
   * Verify the nonce
   */      
  static function verify() {
    $method = filter_input( INPUT_SERVER, 'REQUEST_METHOD', FILTER_SANITIZE_STRING );
    $type = strtoupper( $method ) === 'GET' ? INPUT_GET : INPUT_POST;
    $nonce = filter_input( $type, md5(__CLASS__) . '_n', FILTER_SANITIZE_STRING );
    $code = filter_input( $type, md5(__CLASS__), FILTER_SANITIZE_STRING );
    return ! empty($code) &&  ! empty($nonce) && wp_create_nonce( $code ) === $nonce;
  }

  /**
   * Convert an hexadecimal color to an rgb array color
   */      
  static function hexrgb( $hex ) {
    $int = hexdec( $hex );
    return array(
      "red" => 0xFF & ($int >> 0x10),
      "green" => 0xFF & ($int >> 0x8),
      "blue" => 0xFF & $int
    );
  }

  /**
   * encode the code to avoid put plan code in image url
   */
  private static function encode( $code ) {
    $iv_size = mcrypt_get_iv_size( MCRYPT_BLOWFISH , MCRYPT_MODE_ECB );
    $iv = mcrypt_create_iv( $iv_size, MCRYPT_RAND );
    $key = md5( __CLASS__ );
    $enc = mcrypt_encrypt( MCRYPT_BLOWFISH , $key, $code, MCRYPT_MODE_ECB, $iv);
    return urlencode( base64_encode( $enc ) );
  }

  /**
   * Decode the code
   */
  static function decode( $encoded ) {
    $code = urldecode( base64_decode( $encoded ) );
    $iv_size = mcrypt_get_iv_size( MCRYPT_BLOWFISH , MCRYPT_MODE_ECB );
    $iv = mcrypt_create_iv( $iv_size, MCRYPT_RAND );
    $key = md5( __CLASS__ );
    return mcrypt_decrypt( MCRYPT_BLOWFISH , $key, $code, MCRYPT_MODE_ECB, $iv );
  }

}

Please be sure there is no spaces or blank lines at the strat (before <?php) nor at the end of the file.

The class require another file in your theme folder captcha_img.php that should be put in your theme folder. This file will contain the code you posted, just a little modified:

<?php 
// require the class
@require_once __DIR__ . '/mycaptcha.php';

// defaults
$w = 110;
$h = 35; 
$random_dots = 0;
$random_lines = 20;
$captcha_text_color = "0x#B9B098";
$captcha_noice_color = "0x#CEC7BA";

/* get code */
$code = filter_input( INPUT_GET, 'c', FILTER_SANITIZE_STRING );
if ( empty( $code ) ) die( '' );
$code = MyCaptcha::decode( $code );

/* get settings */
$sizes = filter_input( INPUT_GET, 'size', FILTER_SANITIZE_STRING );
if ( ! empty( $sizes ) ) {
  $s = explode( 'x', $sizes );
  if ( is_numeric($s[0]) && (int) $s[0] > 0  ) $w = (int) $s[0];
  if ( isset($s[1]) && is_numeric($s[1]) && (int) $s[1] > 0  ) $h = (int) $s[1];
}
$dots = (int) filter_input( INPUT_GET, 'dots', FILTER_SANITIZE_NUMBER_INT );
if ( $dots > 0 ) $random_dots = $dots;
$lines = (int) filter_input( INPUT_GET, 'lines', FILTER_SANITIZE_NUMBER_INT );
if ( $dots > 0 ) $random_lines = $lines;
$tcolor = (string) filter_input( INPUT_GET, 'tcolor', FILTER_SANITIZE_STRING );
if ( strlen($tcolor) === 6 ) $captcha_text_color = "0x#{$tcolor}";
$ncolor = (string) filter_input( INPUT_GET, 'ncolor', FILTER_SANITIZE_STRING );
if ( strlen($ncolor) === 6 ) $captcha_noice_color = "0x#{$ncolor}";

/* prepare image */
$font="./monofont.ttf";
$font_size = $h * 0.75;
$image = @imagecreate($w, $h);
$bg_color = imagecolorallocate($image, 255, 255, 255);
$arr_text_color = MyCaptcha::hexrgb($captcha_text_color);
$text_color = imagecolorallocate(
  $image,
  $arr_text_color['red'], 
  $arr_text_color['green'],
  $arr_text_color['blue']
);
$arr_noice_color = MyCaptcha::hexrgb($captcha_noice_color);
  $image_noise_color = imagecolorallocate(
  $image,
  $arr_noice_color['red'], 
  $arr_noice_color['green'],
  $arr_noice_color['blue']
);

/* generating dots randomly in background of image */
for( $i=0; $i < $random_dots; $i++ ) {
  imagefilledellipse(
    $image,
    mt_rand(0, $w), mt_rand(0, $h),
    2, 3,
    $image_noise_color
  );
}

/* generating lines randomly in background of image */
for( $i=0; $i < $random_lines; $i++ ) {
  imageline(
    $image,
    mt_rand(0, $w), mt_rand(0, $h),
    mt_rand(0, $w), mt_rand(0, $h),
    $image_noise_color
  );
}

/* create a text box and add 6 letters code in it */
$textbox = imagettfbbox( $font_size, 0, $font, $code ); 
$x = ( $w - $textbox[4] ) / 2;
$y = ( $h - $textbox[5] ) / 2;
imagettftext( $image, $font_size, 0, $x, $y, $text_color, $font , $code);

/* output captcha image */
header('Content-Type: image/jpeg');
imagejpeg($image);
imagedestroy($image); 

Again, be sure there is no spaces or blank lines at the start (before <?php) nor at the end of the file.

How to use

Print fields

In the forms simple use <?php new MyCaptcha; ?> to output the 3 fields using default settings.

You can customize the number of letters, the width and the height of the image, the colors, the number of lines and dots passing an array of arguments to constructor, e.g.

 <div class="my_captcha">
   <?php
   $args = array(
     'chars_num'=> '8', // number of characters
     'tcolor' => 'FFFFFF', // text color
     'ncolor' => '999999', // noise color
     'dots' => 50, // number of dots
     'lines'=> 40 // number of lines,
     'width'=>'220' // number of width,
     'height'=>'70' // number of height
   );
   new MyCaptcha( $args );
   ?>
 </div>

Verify

In the file or in the function that process the form you can verify simply using:

$is_captcha_corrent = MyCaptcha::verify();

Nothing more. Of course, if you use an external file to process the form, this file have to bootstrap WordPress.

Both files are available in a Gist here.