Location: PHPKode > scripts > AMDev_Captcha > amdev_captcha/AMDev.Tools.Captcha.php
<?php

/**
  * PHP-Class AMDev_Captcha Version 1.0.1, released 29-10-2007
  *
  * Author: Axel Pardemann, hide@address.com
  *
  * Download: 
  *
  * License: GNU LGPL (http://www.opensource.org/licenses/lgpl-license.html)
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
  * License as published by the Free Software Foundation; either
  * version 2.1 of the License, or (at your option) any later version.
  *
  * This library is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  * Lesser General Public License for more details.
  *
  * Notes:
  * - Based int the class "hn_captcha version 1.4" by Horst Nogajski, hide@address.com [http://www.phpclasses.org/browse/package/1569.html]
  * - Uses Xajax Library v0.5beta4 compiled (optional for refreshing the captcha) with some minor modifications explained in the notes.txt file
  *
  **/
  
// @@@ class AMDev_Captcha @@@
class AMDev_Captcha
{
	// === Constants ===
	
	// Data field names used for the class
	const DATA_CAPTCHA_BLOCK = "amdev_captcha";
	const DATA_CAPTCHA_IMG = "amdev_captcha_img";
	const DATA_PUBLIC_KEY = "amdev_captcha_pub_key";
	const DATA_PRIVATE_KEY = "amdev_captcha_priv_key";
	const DATA_SESSION_INFO = "amdev_captcha_ses";
	const DATA_CAPTCHA_REFRESH = "amdev_captcha_refresh";
	
	// Default configuration values
	const DEFAULT_DEBUG_ENABLED = false;
	const DEFAULT_CAPTCHA_PATH = "captcha/images";
	const DEFAULT_CAPTCHA_PREFIX = "amdev_captcha_";
	const DEFAULT_CAPTCHA_CLASS = "";
	const DEFAULT_FONT_PATH = "captcha/fonts";
	const DEFAULT_FONT_COLLECTION = "AUTO";
	const DEFAULT_CHAR_NUMBER = 6;
	const DEFAULT_CHAR_MIN_SIZE = 15;
	const DEFAULT_CHAR_MAX_SIZE = 25;
	const DEFAULT_CHAR_MAX_ROTATION = 25;
	const DEFAULT_CASE_SENSITIVE = false;
	const DEFAULT_MAX_TRIES = 3;
	const DEFAULT_SESSION_INFO_LEN = 32;
	const DEFAULT_SECRET_POS = 12;
	const DEFAULT_SECRET_PHRASE = "There are 10 kinds of people in the world: those who understand binary and those who don't.";
	const DEFAULT_NOISE_ENABLED = true;
	const DEFAULT_NOISE_FACTOR = 9;
	const DEFAULT_LANGUAGE_FILE = "";
	const DEFAULT_FORM_METHOD = "POST";
	const DEFAULT_REFRESH_ENABLED = false;
	const DEFAULT_ONLY_MD5 = false;
	const DEFAULT_WEB_SAFE_COLORS = false;
	const DEFAULT_JPEG_QUALITY = 80;
	const DEFAULT_XAJAX_INC_FILES = "captcha/xajax/xajaxAIO.inc.php";
	const DEFAULT_XAJAX_JS_PATH = "captcha/xajax";
	const DEFAULT_XAJAX_DEBUG_ENABLED = false;
	const DEFAULT_GC_ENABLED = true;
	const DEFAULT_GC_IDLE_COUNT = 100;
	const DEFAULT_GC_CAPTCHA_PERIOD = 600;
	
	// Default messages used in this class
	const LANGUAGE_MESSAGE_SECTION = "AMDev Captcha Language Messages";
	const LANGUAGE_MESSAGES_LANGUAGE = "English";
	const MSG_DEFAULT_TYPE_DISPLAYED_CHARS = "Type in the textfield the characters you see in the image below:";
	const MSG_DEFAULT_CANNOT_READ_CHARS = "If you cannot read the characters displayed in the image click the button to generate a new one.";
	const MSG_DEFAULT_MSG_BUTTON_REFRESH = "Generate new";
	const MSG_DEFAULT_CASE_SENSITIVE = "(The characters are case-sensitive)";
	const MSG_DEFAULT_IMG_ALT_TEXT = "This is a captcha-picture. It is used to prevent mass-access by robots. (see: www.captcha.net)";
	const MSG_DEFAULT_TEXTBOX_LABEL = "Code: ";
	
	// Default error messages used in this class
	const LANGUAGE_ERROR_MESSAGE_SECTION = "AMDev Captcha Language Error Messages";
	const ERR_INVALID_CHARS = "The characters in the image are not correct. Please try again.";
	const ERR_MAX_TRIES_REACHED = "Reached the maximum number of tries with no success.";
	
	// GC constants
	const GC_FILENAME = "amdev_captcha_gc.cfg";
	
	// === end Constants ===
	
	// === Attributes ===
	
		// *** Private Attributes ***
			private $a_Xajax = NULL;
			private $a_XajaxCallback = NULL;
			private $a_XajaxLoaded = false;
			private $a_Debug = false;
			private $a_GDVersion = 0;
			private $a_HackRedirect = "/";
			private $a_CurrentFont = "";
			private $a_NoiseChars = 0;
			private $a_ImageWidth = 0;
			private $a_ImageHeight = 0;
			private $a_QueryString = "";
			private $a_RefreshUrl = "";
			private $a_PublicKey = "";
			private $a_PrivateKey = "";
			private $a_CurrentTry = 0;
			private $a_Hash = "";
			private $a_ShortHash = "";
			private $a_UsedChars = "A..Z";
			private $a_GCFilename = "";
			private $a_GCError = false;
			
			// This array defines the config keys allowed. Any key not contained in this array will be discarded if reading in secure mode.
			// Initial values are passed within this array.
			private $a_Config = array(
				"DEBUG_ENABLED"					=>	AMDev_Captcha::DEFAULT_DEBUG_ENABLED,				// bool: If true then debugging is enabled.
				"CAPTCHA_PATH"					=>	AMDev_Captcha::DEFAULT_CAPTCHA_PATH,				// string: Absolute or relative path to where the images will be saved. (Must be writable and must be in the same published web-tree as the script).
				"CAPTCHA_PREFIX"				=>	AMDev_Captcha::DEFAULT_CAPTCHA_PREFIX,				// string: Prefix to be used for the image filenames.
				"CAPTCHA_CLASS"					=>	AMDev_Captcha::DEFAULT_CAPTCHA_CLASS,				// string: CSS class name for the displayed HTML tags.
				"FONT_PATH"						=>	AMDev_Captcha::DEFAULT_FONT_PATH,					// string: Absolute or relative path to where the fonts collection is.
				"FONT_COLLECTION"				=>	AMDev_Captcha::DEFAULT_FONT_COLLECTION,				// mixed[array(strings)|string]: The font or array of fonts to be used. AUTO takes all fonts in FONT_PATH.
				"CHAR_NUMBER"					=>	AMDev_Captcha::DEFAULT_CHAR_NUMBER,					// integer: Length of the text string to be generated.
				"CHAR_MIN_SIZE"					=>	AMDev_Captcha::DEFAULT_CHAR_MIN_SIZE,				// integer: Minimum size for a char in the captcha.
				"CHAR_MAX_SIZE"					=>	AMDev_Captcha::DEFAULT_CHAR_MAX_SIZE,				// integer: Maximum size for a char in the captcha.
				"CHAR_MAX_ROTATION"				=>	AMDev_Captcha::DEFAULT_CHAR_MAX_ROTATION,			// integer: The maximum degrees a Char should be rotated. Set it to 30 means a random rotation between -30 and 30.
				"CASE_SENSITIVE"				=>	AMDev_Captcha::DEFAULT_CASE_SENSITIVE,				// bool: If true the private key is case-sensitive.
				"MAX_TRIES"						=>	AMDev_Captcha::DEFAULT_MAX_TRIES,					// integer: Maximum input attempts before considering it an attack.
				"SESSION_INFO_LEN"				=>	AMDev_Captcha::DEFAULT_SESSION_INFO_LEN,			// integer: The length of the session info string.
				"SECRET_POS"					=>	AMDev_Captcha::DEFAULT_SECRET_POS,					// integer[1, SESSION_INFO_LEN]: Position in a 32 length string where the session tries are stored (must be less or equal to SESSION_INFO_LEN).
				"SECRET_PHRASE"					=>	AMDev_Captcha::DEFAULT_SECRET_PHRASE,				// string: A "secret" string to generate the md5 hash key.
				"NOISE_ENABLED"					=>	AMDev_Captcha::DEFAULT_NOISE_ENABLED,				// bool: If enabled captcha will be rendered with background noise, if not then just with a grid.
				"NOISE_FACTOR"					=>	AMDev_Captcha::DEFAULT_NOISE_FACTOR,				// integer: Background noise factor.
				"LANGUAGE_FILE"					=>	AMDev_Captcha::DEFAULT_LANGUAGE_FILE,				// string: Absolute or relative path to the language file to be used for the text outputs.
				"FORM_METHOD"					=>	AMDev_Captcha::DEFAULT_FORM_METHOD,					// string[POST|GET]: The method the form is using to send the data.
				"REFRESH_ENABLED"				=>	AMDev_Captcha::DEFAULT_REFRESH_ENABLED,				// bool: If enabled the user can requesta a captcha regeneration without modifying the try count.
				"ONLY_MD5"						=>	AMDev_Captcha::DEFAULT_ONLY_MD5,					// bool: If true it uses only the md5 function for encoding.
				"WEB_SAFE_COLORS"				=>	AMDev_Captcha::DEFAULT_WEB_SAFE_COLORS,				// bool: If true the image will only use web safe colors.
				"JPEG_QUALITY"					=>	AMDev_Captcha::DEFAULT_JPEG_QUALITY,				// integer: Quality to be used for JPEG output image.
				"XAJAX_INC_FILES"				=>	AMDev_Captcha::DEFAULT_XAJAX_INC_FILES,				// mixed[array(strings)|string]: The relative paths to the include files for xajax.
				"XAJAX_JS_PATH"					=>	AMDev_Captcha::DEFAULT_XAJAX_JS_PATH,				// string: Absolute or relative path to the Xajax JS core file.
				"XAJAX_DEBUG_ENABLED"			=>	AMDev_Captcha::DEFAULT_XAJAX_DEBUG_ENABLED,			// bool: If true then Xajax debug is enabled.
				"GC_ENABLED"					=>	AMDev_Captcha::DEFAULT_GC_ENABLED,					// bool: If true the GC is enabled.
				"GC_IDLE_COUNT"					=>	AMDev_Captcha::DEFAULT_GC_IDLE_COUNT,				// integer: Number of captchas to generate before calling GC.
				"GC_CAPTCHA_PERIOD"				=>	AMDev_Captcha::DEFAULT_GC_CAPTCHA_PERIOD			// integer: Captcha alive period in seconds before GC collects it.
								);
			
			// This array defines the default messages for the captcha class (language = english).
			// Initial values are passed within this array.			
			private $a_Messages = array(
				"LANGUAGE"						=>	AMDev_Captcha::LANGUAGE_MESSAGES_LANGUAGE,
				"MSG_TYPE_DISPLAYED_CHARS"		=>	AMDev_Captcha::MSG_DEFAULT_TYPE_DISPLAYED_CHARS,
				"MSG_CANNOT_READ_CHARS"			=>	AMDev_Captcha::MSG_DEFAULT_CANNOT_READ_CHARS,
				"MSG_BUTTON_REFRESH"			=>	AMDev_Captcha::MSG_DEFAULT_MSG_BUTTON_REFRESH,
				"MSG_CASE_SENSITIVE"			=>	AMDev_Captcha::MSG_DEFAULT_CASE_SENSITIVE,
				"MSG_IMG_ALT_TEXT"				=>	AMDev_Captcha::MSG_DEFAULT_IMG_ALT_TEXT,
				"MSG_TEXTBOX_LABEL"				=>	AMDev_Captcha::MSG_DEFAULT_TEXTBOX_LABEL
								);
								
			private $a_Errors = array(
				"ERR_INVALID_CHARS"				=>	AMDev_Captcha::ERR_INVALID_CHARS,
				"ERR_MAX_TRIES_REACHED"			=>	AMDev_Captcha::ERR_MAX_TRIES_REACHED
								);
		// *** end Private Attributes ***
		
	// === end Attributes ===
	
	// === Constructor ===
	function AMDev_Captcha($config=array(), $secure=true)
	{		
		// Set debugging mode
		if(array_key_exists("DEBUG_ENABLED", $config)) $this->a_Config["DEBUG_ENABLED"] = $config["DEBUG_ENABLED"];
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Captcha debug is enabled. Image refresh won't work as long as debug-mode is on.";
		
		// Get GD Library version if GD not present then die.
		$this->a_GDVersion = $this->GetGDVersion(true);
		if($this->a_GDVersion === 0) die("<br />Fatal Error: There is no GD-Library-Support enabled. AMDev.Captcha cannot be used");
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: The available GD-Library has version ".$this->a_GDVersion;
		
		// Hack prevention
		if($this->IsHack())
		{
			if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Hacking attempt discovered";
			if(isset($this->a_HackRedirect) && !headers_sent()) header("Location: ".$this->a_HackRedirect);
			else die("<br />Fatal Error: Hack prevented.");
		}
		
		// Read captcha configuration. Discard not allowed keys if reading in secure mode.
		if(is_array($config))
		{
			// Reset the array pointer to the first element.
			reset($config);
			if($secure && strcmp("4.2.0", phpversion()) < 0)
			{
				if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Read configuration in secure-mode";
				foreach($config as $key=>$val)
				{
					if(array_key_exists($key, $this->a_Config)) $this->a_Config[$key] = $val;
				}
			}
			else
			{
				if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Read configuration in unsecure-mode";
				foreach($config as $key=>$val)
				{
					$this->a_Config[$key] = $val;
				}
			}
		}
		
		// Correct values that are out of range or wrong
		if($this->a_Config["MAX_TRIES"] > 9 || $this->a_Config["MAX_TRIES"] < 1) $this->a_Config["MAX_TRIES"] = AMDev_Captcha::DEFAULT_MAX_TRIES;
		if($this->a_Config["SESSION_INFO_LEN"] < 1) $this->a_Config["SESSION_INFO_LEN"] = AMDev_Captcha::DEFAULT_SESSION_INFO_LEN;
		if($this->a_Config["SECRET_POS"] > $this->a_Config["SESSION_INFO_LEN"] || $this->a_Config["SECRET_POS"] < 1) $this->a_Config["SECRET_POS"] = AMDev_Captcha::DEFAULT_SECRET_POS;
		if($this->a_Config["CHAR_MIN_SIZE"] > $this->a_Config["CHAR_MAX_SIZE"])
		{
			$temp = $this->a_Config["CHAR_MIN_SIZE"];
			$this->a_Config["CHAR_MIN_SIZE"] = $this->a_Config["CHAR_MAX_SIZE"];
			$this->a_Config["CHAR_MAX_SIZE"] = $temp;
		}
		$this->a_Config["FORM_METHOD"] = strtoupper($this->a_Config["FORM_METHOD"]);
		if($this->a_Config["FORM_METHOD"] !== "POST" && $this->a_Config["FORM_METHOD"] !== "GET") $this->a_Config["FORM_METHOD"] = AMDev_Captcha::DEFAULT_FORM_METHOD;
		
		// Sanitize captcha path
		$this->a_Config["CAPTCHA_PATH"] = $this->SanitizePath($this->a_Config["CAPTCHA_PATH"], true, false);
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Captcha path is: (".$this->a_Config["CAPTCHA_PATH"].")";
		
		// If Captcha path doesn't exist then create it, else verify writing capabilities for Owner, Group and World
		if(realpath($this->a_Config["CAPTCHA_PATH"]) === false)
		{
			mkdir($this->a_Config["CAPTCHA_PATH"], 0777, true);
			if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Captcha path didn't exist. Tried to create it with output: ".((realpath($this->a_Config["CAPTCHA_PATH"]) === false)? "error" : "ok (".realpath($this->a_Config["CAPTCHA_PATH"]).")");
		}
		else
		{
			$perms = $this->GetFilePerms(realpath($this->a_Config["CAPTCHA_PATH"]));
			if($perms["TYPE"] === "Directory")
			{
				if(!($perms["OWNER"]["WRITE"] && $perms["GROUP"]["WRITE"] && $perms["WORLD"]["WRITE"]))
				{
					if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Captcha path doesn't have the correct permissions. Current permission string [".$perms["STRING"]."]";
				}
			}
			else
			{
				if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Captcha path is not a directory. Path type [".$perms["Type"]."]";
			}
		}
		
		// Sanitize font path
		$this->a_Config["FONT_PATH"] = $this->SanitizePath($this->a_Config["FONT_PATH"], true, false);
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Font path is: (".$this->a_Config["FONT_PATH"].")";
		
		// Verify Font Collection
		if(is_array($this->a_Config["FONT_COLLECTION"]))
		{
			if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Verifying Font Collection (".count($this->a_Config["FONT_COLLECTION"]).")";
			if(realpath($this->a_Config["FONT_PATH"]) !== false)
			{
				foreach($this->a_Config["FONT_COLLECTION"] as $key => $val)
				{
					$strFontFilename = realpath($this->a_Config["FONT_PATH"].$val);
					if(!($strFontFilename !== false) || !is_readable($strFontFilename))
					{
						unset($this->a_Config["FONT_COLLECTION"][$key]);
					}
				}
				if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Valid Font files: (".count($this->a_Config["FONT_COLLECTION"]).")";
				if(count($this->a_Config["FONT_COLLECTION"]) < 1) die("<br />Fatal Error: No Fonts available for the Captcha");
			}else
			{
				die("<br />Fatal Error: The given font path doesn't exist. Can't read fonts for Captcha");
			}
		}
		else
		{
			if(strtoupper($this->a_Config["FONT_COLLECTION"]) === "AUTO")
			{
				if(($strFontPath = realpath($this->a_Config["FONT_PATH"])) !== false)
				{
					$this->a_Config["FONT_COLLECTION"] = array();
					if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Reading font path for fonts";
					
					$strFontPath = $this->SanitizePath($strFontPath, true, true);
							
					// Read font path for fonts
					if($fonts_dir = opendir($strFontPath))
					{
						while($file = @readdir($fonts_dir))
						{
							if(substr(strtolower($file), -4) != ".ttf")
							{
								continue;
							}
							if(is_readable($strFontPath.$file))
							{
								$this->a_Config["FONT_COLLECTION"][] = $file;
							}
						}
						closedir($fonts_dir);
					}
					else
					{
						die("<br />Fatal Error: Cannot open fonts dir");
					}
					if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Valid Font files: (".count($this->a_Config["FONT_COLLECTION"]).")";
					if(count($this->a_Config["FONT_COLLECTION"]) < 1) die("<br />Fatal Error: No Fonts available for the Captcha");
				}
				else
				{
					die("<br />Fatal Error: The given font path doesn't exist. Can't read fonts for Captcha");
				}
			}
			else
			{
				if(($strFontPath = realpath($this->a_Config["FONT_PATH"])) !== false)
				{
					if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Checking the given Font file (".$this->a_Config["FONT_COLLECTION"].")";
					if(!is_readable($strFontPath.$this->a_Config["FONT_COLLECTION"])) die("<br />Fatal Error: No Fonts available for the Captcha");
				}
				else
				{
					die("<br />Fatal Error: The given font path doesn't exist. Can't read fonts for Captcha");
				}
			}
		}
		
		// Set the current font to something
		$this->SetCurrentFont();
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Current Font set to: (".$this->a_CurrentFont.")";
		
		// Calculate background noise factor
		$this->a_NoiseChars = ($this->a_Config["NOISE_ENABLED"])? ($this->a_Config["CHAR_NUMBER"] * $this->a_Config["NOISE_FACTOR"]) : 0;
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Set number of noise characters to: (".$this->a_NoiseChars.")";
		
		// Set image dimension depending on CHAR_MAX_SIZE and CHAR_MIN_SIZE
		$this->a_ImageWidth = ($this->a_Config["CHAR_NUMBER"] + 1) * (int)(($this->a_Config["CHAR_MAX_SIZE"] + $this->a_Config["CHAR_MIN_SIZE"]) / 1.5);
		$this->a_ImageHeight = (int)(2.4 * $this->a_Config["CHAR_MAX_SIZE"]);
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Set image dimensions to: (".$this->a_ImageWidth."x".$this->a_ImageHeight.")";
		
		// Set the used charecter string
		$this->a_UsedChars = ($this->a_Config["ONLY_MD5"])? "A..F" : "A..Z";
		
		// Read language file
		$this->ReadLanguageFile($this->a_Config["LANGUAGE_FILE"]);
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Language set to (".$this->a_Messages["LANGUAGE"].")";
		
		// Keep params from original GET request
		$this->a_QueryString = (strlen(trim(@$_SERVER["QUERY_STRING"])) > 0)? "?".strip_tags($_SERVER["QUERY_STRING"]) : "";
		$this->a_RefreshUrl = $_SERVER["SCRIPT_NAME"].$this->a_QueryString;
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Keep params from original GET request: (".$this->a_QueryString.")";
		
		// Get Form Data
		$pubKey  = $this->GetFormData(AMDev_Captcha::DATA_PUBLIC_KEY);
		if($pubKey !== NULL) $this->a_PublicKey = substr($pubKey, 0, $this->a_Config["CHAR_NUMBER"]);
		$privKey = $this->GetFormData(AMDev_Captcha::DATA_PRIVATE_KEY);
		if($privKey !== NULL) $this->a_PrivateKey = substr($privKey, 0, $this->a_Config["CHAR_NUMBER"]);
		$tries  = $this->GetFormData(AMDev_Captcha::DATA_SESSION_INFO);
		$this->a_CurrentTry = ($tries === NULL)? 0 : $this->GetTryCount();
		$this->a_CurrentTry++;
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Getting captcha info from form. Current try is: (".$this->a_CurrentTry.")";

		// Generate Hash
		$this->a_Hash = md5($this->a_Config["SECRET_PHRASE"]);
		$this->a_ShortHash = substr(md5(uniqid(rand(), true)), 0, $this->a_Config["CHAR_NUMBER"]);
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Generate hash, short hash is: (".$this->a_ShortHash.")";
		
		// Include Xajax PHP Libraries		***WARNING*** Check extension to php (Not done yet)
		$allIncluded = true;
		if(is_array($this->a_Config["XAJAX_INC_FILES"]))
		{
			for($i = 0; $i < count($this->a_Config["XAJAX_INC_FILES"]); $i++)
			{
				$filename = realpath($this->a_Config["XAJAX_INC_FILES"][$i]);
				if(!@include $filename)
				{
					$allIncluded = false;
				}
			}
		}
		else
		{
			$filename = realpath($this->a_Config["XAJAX_INC_FILES"]);
			if(!@include $filename)
			{
				$allIncluded = false;
			}
		}
		$this->a_XajaxLoaded = $allIncluded;
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Xajax Libraries loaded: (".(($this->a_XajaxLoaded)? "true" : "false").")";
		
		// Configure Xajax
		$this->a_Xajax = new xajax();
		if($this->a_Config["XAJAX_DEBUG_ENABLED"]) $this->a_Xajax->setFlag("debug", true);
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Xajax Debug enabled";
		
		// Sanitize Xajax JS path
		if($this->a_Config["XAJAX_JS_PATH"] === ".") $this->a_Config["XAJAX_JS_PATH"] = dirname($_SERVER["SCRIPT_NAME"]);
		$this->a_Config["XAJAX_JS_PATH"] = str_replace(array("\\"), array("/"), $this->a_Config["XAJAX_JS_PATH"]);
		$this->a_Config["XAJAX_JS_PATH"] = (substr($this->a_Config["XAJAX_JS_PATH"], -1) === "/")? $this->a_Config["XAJAX_JS_PATH"] : $this->a_Config["XAJAX_JS_PATH"]."/";
		$this->a_Xajax->configure("javascript URI", $this->a_Config["XAJAX_JS_PATH"]);
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Xajax JS path is: (".$this->a_Config["XAJAX_JS_PATH"].")";
		
		// Register Xajax callback function
		//$this->a_XajaxCallback =& $this->a_Xajax->registerFunction(array("AMDevCaptcha_RefreshCaptcha", $this, "RefreshCaptcha"));
		$this->a_XajaxCallback =& $this->a_Xajax->register(XAJAX_FUNCTION, array("AMDevCaptcha_RefreshCaptcha", $this, "RefreshCaptcha"));
		$this->a_XajaxCallback->setParameter(0, XAJAX_JS_VALUE, "document.getElementById('".AMDev_Captcha::DATA_PUBLIC_KEY."').value");
		$this->a_XajaxCallback->setParameter(1, XAJAX_JS_VALUE, "document.getElementById('".AMDev_Captcha::DATA_SESSION_INFO."').value");
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Xajax callback function registered";
		
		// Process any Xajax request
		$this->a_Xajax->processRequest();
		if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Xajax is processing requests";
		
		// Configure Garbage Collector only if it is enabled
		if($this->a_Config["GC_ENABLED"])
		{
			if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: GC enabled";
		
			$this->a_GCFilename = $this->a_Config["CAPTCHA_PATH"].AMDev_Captcha::GC_FILENAME;
			if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: GC filename is (".$this->a_GCFilename.")";
			
			// Retrieve GC last counter value
			$test = $this->GCFileCounter($this->a_GCFilename);
			if(!$test) $test = $this->CreateGCCounterFile($this->a_Config["CAPTCHA_PATH"].$this->a_GCFilename);
			
			// Set and retrieve current counter value
			$counter = $this->GCFileCounter($this->a_GCFilename, true);
			
			// Check if GC counter is working
			if(($counter !== false) && ($counter - $test == 1))
			{
				// Counter works perfect
				if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br>-Captcha-Debug: Current GC counter value is (".$counter."). GC should start collecting at (".$this->a_Config["GC_IDLE_COUNT"].")";
	
				// Check if GC should collect
				if($counter >= $this->a_Config["GC_IDLE_COUNT"])
				{
					// Reset GC counter
					$this->GCFileCounter($this->a_GCFilename, true, 0);
					if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br>-Captcha-Debug: Reset GC counter value (0) and prepare to collect";
	
					// Set GC to collect
					$this->a_GCError = ($this->GCCollect())? false : true;
					if($this->a_Config["DEBUG_ENABLED"] && $this->a_GCError) echo "\r\n<br>-Captcha-Debug: GC error! Some trashfiles couldn't be deleted";
				}
	
			}
			else
			{
				// GC counter error
				$this->a_GCError = true;
				if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: GC error! No counter value available";
			}
		}
	}
	// === end Constructor ===
	
	// === Private Methods ===
	
		// *** void ReadLanguageFile() ***
		private function ReadLanguageFile($filename)
		{
			if($filename !== "")
			{
				$pathInfo = pathinfo($filename);
				
				// Sanitize path
				$pathInfo["dirname"] = $this->SanitizePath($pathInfo["dirname"], true, false);
				
				if(realpath($pathInfo["dirname"].$pathInfo["basename"]) !== false)
				{
					$absolutePath = realpath($pathInfo["dirname"].$pathInfo["basename"]);
					if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Reading language file: (".$absolutePath.")";
					
					// Parse language file as INI file. Ignore "dummy" keys in file.
					$parser = new AMDev_LanguageFileParser();
					if($parser->ParseFile($absolutePath))
					{
						foreach($this->a_Messages as $key => $val)
						{
							$newVal = $parser->Get(AMDev_Captcha::LANGUAGE_MESSAGE_SECTION, $key);
							if($newVal !== NULL) $this->a_Messages[$key] = $this->SanitizeOutputText($newVal);
						}
						foreach($this->a_Errors as $key => $val)
						{
							$newVal = $parser->Get(AMDev_Captcha::LANGUAGE_ERROR_MESSAGE_SECTION, $key);
							if($newVal !== NULL) $this->a_Errors[$key] = $this->SanitizeOutputText($newVal);
						}
					}
					else
					{
						if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Couldn't parse language file (".$filename.").The default language will be used.";
					}
				}
				else
				{
					if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Path (".$filename.") doesn't exist. Using default language.";
				}
			}
		}
		// *** end ReadLanguageFile() ***
		
		// *** void MakeCaptcha() ***
		private function MakeCaptcha()
        {
            $private_key = $this->GeneratePrivateKey();
            if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Generate private key: ($private_key)";

            // Create Image and set the appropriate function depending on GD-Version & websafecolor-value
            if($this->a_GDVersion >= 2 && !$this->a_Config["WEB_SAFE_COLORS"])
            {
                $func1 = "imagecreatetruecolor";		// PHP GD Library function
                $func2 = "imagecolorallocate";			// PHP GD Library function
            }
            else
            {
                $func1 = "imageCreate";					// PHP GD Library function
                $func2 = "imagecolorclosest";			// PHP GD Library function
            }
            $image = $func1($this->a_ImageWidth, $this->a_ImageHeight);
            if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Generate ImageStream with: ($func1())";
            if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: For color definitions we use: ($func2())";


            // Set BackgroundColor
            $rgb = $this->RandomColor(224, 255);
            $back =  @imagecolorallocate($image, $rgb["R"], $rgb["G"], $rgb["B"]);
            @ImageFilledRectangle($image, 0, 0, $this->a_ImageWidth, $this->a_ImageHeight, $back);
            if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: We allocate one color for Background: (".$rgb["R"].", ".$rgb["G"].", ".$rgb["B"].")";

            // Allocates the 216 websafe color palette to the image
            if($this->a_GDVersion < 2 || $this->a_Config["WEB_SAFE_COLORS"]) $this->ConvertToWebSafe($image);

            // Fill with noise or grid
            if($this->a_NoiseChars > 0)
            {
                // Random characters in background with random position, angle, color
                if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Fill background with noise chars: (".$this->a_NoiseChars.")";
                for($i=0; $i < $this->a_NoiseChars; $i++)
                {
                    srand((double)microtime()*1000000);
                    $size = intval(rand((int)($this->a_Config["CHAR_MIN_SIZE"] / 2.3), (int)($this->a_Config["CHAR_MAX_SIZE"] / 1.7)));
                    srand((double)microtime()*1000000);
                    $angle = intval(rand(0, 360));
                    srand((double)microtime()*1000000);
                    $x = intval(rand(0, $this->a_ImageWidth));
                    srand((double)microtime()*1000000);
                    $y = intval(rand(0, (int)($this->a_ImageHeight - ($size / 5))));
                    $rgb = $this->RandomColor(160, 224);
                    $color = $func2($image, $rgb["R"], $rgb["G"], $rgb["B"]);
                    srand((double)microtime()*1000000);
                    $text = chr(intval(rand(45,250)));
                    @ImageTTFText($image, $size, $angle, $x, $y, $color, $this->SetCurrentFont(), $text);
                }
            }
            else
            {
                // Generate grid
                if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Fill background with x-gridlines: (".(int)($this->a_ImageWidth / (int)($this->a_Config["CHAR_MIN_SIZE"] / 1.5)).")";
                for($i=0; $i < $this->a_ImageWidth; $i += (int)($this->a_Config["CHAR_MIN_SIZE"] / 1.5))
                {
                    $rgb = $this->RandomColor(160, 224);
                    $color = $func2($image, $rgb["R"], $rgb["G"], $rgb["B"]);
                    @imageline($image, $i, 0, $i, $this->a_ImageHeight, $color);
                }
                if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Fill background with y-gridlines: (".(int)($this->a_ImageHeight / (int)(($this->a_Config["CHAR_MIN_SIZE"] / 1.8))).")";
                for($i=0 ; $i < $this->a_ImageHeight; $i += (int)($this->a_Config["CHAR_MIN_SIZE"] / 1.8))
                {
                    $rgb = $this->RandomColor(160, 224);
                    $color = $func2($image, $rgb["R"], $rgb["G"], $rgb["B"]);
                    @imageline($image, 0, $i, $this->a_ImageWidth, $i, $color);
                }
            }

            // Generate text
            if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Fill foreground with chars and shadows: (".$this->a_Config["CHAR_NUMBER"].")";
            for($i=0, $x = intval(rand($this->a_Config["CHAR_MIN_SIZE"], $this->a_Config["CHAR_MAX_SIZE"])); $i < $this->a_Config["CHAR_NUMBER"]; $i++)
            {
                $text = substr($private_key, $i, 1);
                srand((double)microtime()*1000000);
                $angle = intval(rand(($this->a_Config["CHAR_MAX_ROTATION"] * -1), $this->a_Config["CHAR_MAX_ROTATION"]));
                srand((double)microtime()*1000000);
                $size = intval(rand($this->a_Config["CHAR_MIN_SIZE"], $this->a_Config["CHAR_MAX_SIZE"]));
                srand((double)microtime()*1000000);
                $y = intval(rand((int)($size * 1.5), (int)($this->a_ImageHeight - ($size / 7))));
                $rgb = $this->RandomColor(0, 127);
                $color =  $func2($image, $rgb["R"], $rgb["G"], $rgb["B"]);
                $rgb = $this->RandomColor(0, 127);
                $shadow = $func2($image, $rgb["R"] + 127, $rgb["G"] + 127, $rgb["B"] + 127);
                @ImageTTFText($image, $size, $angle, $x + (int)($size / 15), $y, $shadow, $this->SetCurrentFont(), $text);
                @ImageTTFText($image, $size, $angle, $x, $y - (int)($size / 15), $color, $this->a_CurrentFont, $text);
                $x += (int)($size + ($this->a_Config["CHAR_MIN_SIZE"] / 5));
            }
			$filename = realpath($this->a_Config["CAPTCHA_PATH"]);
			if(substr($filename, 0, 1) === "/")
			{
				$filename = $this->SanitizePath($filename, true, true);
			}
			$filename .= $this->GetImageFilename();
            @ImageJPEG($image, $filename, $this->a_Config["JPEG_QUALITY"]);
            $res = file_exists($filename);
            if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Save Image with quality [".$this->a_Config["JPEG_QUALITY"]."] as (".$filename.") returns: (".($res ? 'true' : 'false').")";
            @ImageDestroy($image);
            if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Destroy Imagestream.";
            if(!$res) die("<br />Fatal Error: Unable to save captcha-image.");
        }
		// *** end MakeCaptcha() ***
		
		// *** void ConvertToWebSafe() ***
		private function ConvertToWebSafe(&$image)
        {
            for($r = 0; $r <= 255; $r += 51)
            {
                for($g = 0; $g <= 255; $g += 51)
                {
                    for($b = 0; $b <= 255; $b += 51)
                    {
                        $color = imagecolorallocate($image, $r, $g, $b);
                    }
                }
            }
            if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Allocate 216 websafe colors to image: (".imagecolorstotal($image).")";
        }
		// *** end ConvertToWebSafe() ***
	
	// === end Private Methods ===
	
	// === Private Functions ===
	
		// *** int GetGDVersion() ***
		private function GetGDVersion($major=false)
		{
			if(extension_loaded("gd") && function_exists("gd_info"))
			{
				$stats = gd_info();
				if(preg_match("([\d\.]+)", $stats["GD Version"], $matches))
				{
					$gd_version_number = $matches[0];
				}
				else
				{
					$gd_version_number = "0";
				}
			}
			else
			{
				$gd_version_number = "0";
			}
			
			if($major)
			{
				$ret = (int)substr($gd_version_number, 0, strpos($gd_version_number, "."));
			}
			else
			{
				$pos = strpos($gd_version_number, ".", strpos($gd_version_number, ".") + 1);
				$ret = (float)substr($gd_version_number, 0, ($pos > 0)? $pos : strlen($gd_version_number));
			}
			
			return $ret;
		}
		// *** end GetGDVersion() ***
		
		// *** bool IsHack() ***
		private function IsHack()
		{
			$ret = false;
			
			// Hack prevention code
			
			return $ret;
		}
		// *** end IsHack() ***
		
		// *** string GetFormData() ***
		private function GetFormData($varName)
		{
			if($this->a_Config["FORM_METHOD"] === "POST")
			{
				if(isset($_POST[$varName]))
				{
					return strip_tags($_POST[$varName]);
				}
			}
			if($this->a_Config["FORM_METHOD"] === "GET")
			{
				if(isset($_GET[$varName]))
				{
					return strip_tags($_GET[$varName]);
				}
			}
			return NULL;
		}
		// *** end GetFormData() ***
		
		// *** string SetCurrentFont() ***
		private function SetCurrentFont($fontKey="RANDOM")
		{
			if(is_array($this->a_Config["FONT_COLLECTION"]) && strtoupper($fontKey) === "RANDOM")
			{
				srand((float)microtime() * 10000000);
				$key = array_rand($this->a_Config["FONT_COLLECTION"]);
				$this->a_CurrentFont = $this->a_Config["FONT_PATH"].$this->a_Config["FONT_COLLECTION"][$key];
			}
			else
			{
				$this->a_CurrentFont = $this->a_Config["FONT_PATH"].$this->a_Config["FONT_COLLECTION"];
			}
			return $this->a_CurrentFont;
		}
		// *** end SetCurrentFont() ***
		
		// *** mixed[int|string] GetTryCount() ***
		private function GetTryCount($in=true, $sessionInfo=NULL)
		{
			if($in)
			{
				return (int)substr($this->GetFormData(AMDev_Captcha::DATA_SESSION_INFO), ($this->a_Config["SECRET_POS"] - 1), 1);
			}
			else
			{
				if($sessionInfo !== NULL)
				{
					if(strlen($sessionInfo) === $this->a_Config["SESSION_INFO_LEN"])
					{
						$this->a_CurrentTry = (int)substr($sessionInfo, ($this->a_Config["SECRET_POS"] - 1), 1);
					}
				}

				$a = "";
				$b = "";
				for($i = 1; $i < $this->a_Config["SECRET_POS"]; $i++)
				{
					srand((double)microtime()*1000000);
					$a .= intval(rand(1, $this->a_Config["MAX_TRIES"]));
				}
				for($i = 0; $i < ($this->a_Config["SESSION_INFO_LEN"] - $this->a_Config["SECRET_POS"]); $i++)
				{
					srand((double)microtime()*1000000);
					$b .= intval(rand(1, $this->a_Config["MAX_TRIES"]));
				}
				
				return $a.$this->a_CurrentTry.$b;
			}
		}
		// *** end GetTryCount() ***
		
		// *** string DisplayCaptcha() ***
		private function DisplayCaptcha()
		{
            $this->MakeCaptcha();			
			$imgPath = $this->SanitizePath($this->ConvertToAbsoluteUrlPath($this->a_Config["CAPTCHA_PATH"])).$this->GetImageFilename();
            $imgSize = getimagesize($this->SanitizePath($this->ConvertToRelativeUrlPath($this->a_Config["CAPTCHA_PATH"]), true, false).$this->GetImageFilename());
            $ret = "\r\n<img id=\"".AMDev_Captcha::DATA_CAPTCHA_IMG."\" name=\"".AMDev_Captcha::DATA_CAPTCHA_IMG."\" ".(($this->a_Config["CAPTCHA_CLASS"]!=="")? "class=\"".$this->a_Config["CAPTCHA_CLASS"]."\" " : "")."src=\"".$imgPath."\" ".$imgSize[3]." alt=\"".$this->a_Messages["MSG_IMG_ALT_TEXT"]."\" title=\"AMDev Captcha\">\r\n";
            return $ret;
		}
		// *** end DisplayCaptcha() ***
		
		// *** string GeneratePrivateKey() ***
		private function GeneratePrivateKey($public="")
        {
            if($public === "") $public = $this->a_ShortHash;
            if($this->a_Config["ONLY_MD5"])
            {
                $key = substr(md5($this->a_Hash.$public), 16 - (int)($this->a_Config["CHAR_NUMBER"] / 2), $this->A_Config["CHAR_NUMBER"]);
            }
            else
            {
                $key = substr(base64_encode(md5($this->a_Hash.$public)), 16 - (int)($this->a_Config["CHAR_NUMBER"] / 2), $this->a_Config["CHAR_NUMBER"]);
                $key = strtr($key, '0OoIi1B8+-_/=', 'WXxLL7452369H');
            }
            return $key;
        }
		// *** end GeneratePrivateKey() ***
		
		// *** array[R, G, B] RandomColor() ***
		private function RandomColor($min, $max)
        {
            srand((double)microtime() * 1000000);
            $r = intval(rand($min, $max));
            srand((double)microtime() * 1000000);
            $g = intval(rand($min, $max));
            srand((double)microtime() * 1000000);
            $b = intval(rand($min, $max));
            return array("R"=>$r, "G"=>$g, "B"=>$b);
        }
		// *** end RandomColor() ***
		
		// *** string GetImageFilename() ***
		private function GetImageFilename($public="")
        {
            if($public === "") $public = $this->a_ShortHash;
            return $this->a_Config["CAPTCHA_PREFIX"].$public.".jpg";
        }
		// *** end GetImageFilename() ***
		
		// *** string ConvertToAbsoluteUrlPath() ***
		private function ConvertToAbsoluteUrlPath($path="")
		{
			$doc_root = $this->GetVirtualDocumentRoot($_SERVER["SCRIPT_NAME"]);
			$s = $this->SanitizePath($doc_root["WEB_FOLDER"], false);
			$trailing = str_replace($doc_root["VIRTUAL_ROOT"], "", $this->SanitizePath($path, false));
			if(strlen($trailing) > 0)
			{
				$s .= ($trailing[0] === "/")? $trailing : "/".$trailing;
			}
			
			return $s;
		}
		// *** end ConvertToAbsoluteUrlPath() ***
		
		// *** string str_replace_once() ***
		private function str_replace_once($search, $replace, $subject)
		{
			if(($pos = strpos($subject, $search)) !== false)
			{
				$ret = substr($subject, 0, $pos).$replace.substr($subject, $pos + strlen($search));
			}
			else 
			{
				$ret = $subject;
			}
			
			return $ret;
		}
		// *** end str_replace_once() ***
		
		// *** string ConvertToRelativeUrlPath() ***
		private function ConvertToRelativeUrlPath($path="")
		{
			$doc_root = $this->GetVirtualDocumentRoot($_SERVER["SCRIPT_NAME"]);
			$s = $this->SanitizePath($doc_root["WEB_FOLDER"], false);
			$trailing = str_replace($doc_root["VIRTUAL_ROOT"], "", $this->SanitizePath($path, false));
			if(strlen($trailing) > 0)
			{
				$s .= ($trailing[0] === "/")? $trailing : "/".$trailing;
			}
			$s = $this->str_replace_once($this->SanitizePath($doc_root["WEB_FOLDER"]), "", $s);
			
			return $s;
		}
		// *** end ConvertToRelativeUrlPath() ***
		
		// *** string GetVirtualDocumentRoot() ***
		private function GetVirtualDocumentRoot($localpath)
		{
			$doc_root = array("VIRTUAL_ROOT" => "", "WEB_FOLDER" => "");
			$localpath = $this->SanitizePath(dirname($localpath), false);
			
			$doc_root["WEB_FOLDER"] = $this->SanitizePath($localpath, false, false);
			
			// Get sanitized absolute path for the current directory
			$absolutepath = $this->SanitizePath(realpath("."));
			
			// Get virtual document root
			$pos = (strrpos($absolutepath, $localpath) !== false)? strrpos($absolutepath, $localpath) : strlen($absolutepath);
			$doc_root["VIRTUAL_ROOT"] = $this->SanitizePath(substr($absolutepath, 0, $pos));
			
			return $doc_root;
		}
		// *** end GetVirtualDocumentRoot() ***
		
		// *** string SanitizePath() ***
		private function SanitizePath($path="", $trailingSlash=true, $leadingSlash=true)
		{
			$path = str_replace(array("\\"), array("/"), $path);
			$path = str_replace(array("//"), array("/"), $path);
			
			if($trailingSlash)
			{
				$path = (substr($path, -1) === "/")? $path : $path."/";
			}
			else
			{
				$lastPos = strlen($path) - 1;
				$path = (substr($path, $lastPos) === "/")? substr($path, 0, $lastPos) : $path;
			}
			
			$firstPos = 0;
			if($leadingSlash)
			{
				if(substr($path, $firstPos + 1, 1) !== ":")
				{
					$path = (substr($path, $firstPos, 1) === "/")? $path : "/".$path;
				}
			}
			else
			{
				if(substr($path, $firstPos + 1, 1) !== ":")
				{
					$path = (substr($path, $firstPos, 1) === "/")? substr($path, $firstPos + 1) : $path;
				}
			}
			
			return $path;
		}
		// *** end SanitizePath() ***
		
		// *** string SanitizeOutputText() ***
		function SanitizeOutputText($txt)
        {
            $trans = get_html_translation_table(HTML_ENTITIES);
            $txt = strtr($txt, $trans);
            return str_replace(array('&lt;','&gt;','&amp;nbsp;'), array('<','>','&nbsp;'), $txt);
        }
		// *** end SanitizeOutputText() ***
		
		// *** bool CheckCaptcha ***
		function CheckCaptcha($publicKey, $privateKey)
        {
            $res = "false";
			$imgPath = realpath($this->a_Config["CAPTCHA_PATH"]);
			if(substr($imgPath, 0, 1) === "/")
			{
				$imgPath = $this->SanitizePath($imgPath, true, true);
			}
			$imgPath .= $this->GetImageFilename($publicKey);
			
            // If file exists erase it from disk as it was already displayed once
            if(file_exists($imgPath))
            {
                $res = @unlink($imgPath)? "true" : "false";
                if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Delete image (".$imgPath.") returns: ($res)";
				
				if($this->a_Config["CASE_SENSITIVE"])
				{
					$res = ($privateKey === $this->GeneratePrivateKey($publicKey))? "true" : "false";
				}
				else
				{
                	$res = (strtolower($privateKey) === strtolower($this->GeneratePrivateKey($publicKey)))? "true" : "false";
				}
                if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Comparing public key with private key returns: ($res)";
            }
            return ($res === "true")? true : false;
        }
		// *** end CheckCaptcha() ***
		
		// *** string DisplayCaptchaFormPart() ***
		private function DisplayCaptchaFormPart($current_try=0)
		{
			$allowRefresh = ($this->a_XajaxLoaded && $this->a_Config["REFRESH_ENABLED"]);
			$s = "<input type=\"hidden\" id=\"".AMDev_Captcha::DATA_SESSION_INFO."\" name=\"".AMDev_Captcha::DATA_SESSION_INFO."\" ".(($this->a_Config["CAPTCHA_CLASS"]!=="")? "class=\"".$this->a_Config["CAPTCHA_CLASS"]."\" " : "")."value=\"".$current_try."\">\r\n";
			$s .= "<input type=\"hidden\" id=\"".AMDev_Captcha::DATA_PUBLIC_KEY."\" name=\"".AMDev_Captcha::DATA_PUBLIC_KEY."\" ".(($this->a_Config["CAPTCHA_CLASS"]!=="")? "class=\"".$this->a_Config["CAPTCHA_CLASS"]."\" " : "")."value=\"".$this->a_ShortHash."\">\r\n";
			$s .= $this->a_Messages["MSG_TYPE_DISPLAYED_CHARS"]."<br />\r\n";
            $s .= $this->DisplayCaptcha()."<br />\r\n";
			if($this->a_Config["CASE_SENSITIVE"]) $s .= $this->a_Messages["MSG_CASE_SENSITIVE"]."<br />\r\n";
			if($allowRefresh) $s .= $this->a_Messages["MSG_CANNOT_READ_CHARS"]."<br />\r\n";
            $s .= $this->a_Messages["MSG_TEXTBOX_LABEL"]."<input type=\"text\" id=\"".AMDev_Captcha::DATA_PRIVATE_KEY."\" name=\"".AMDev_Captcha::DATA_PRIVATE_KEY."\" ".(($this->a_Config["CAPTCHA_CLASS"]!=="")? "class=\"textbox_".$this->a_Config["CAPTCHA_CLASS"]."\" " : "")."value=\"\" maxlength=\"".$this->a_Config["CHAR_NUMBER"]."\" size=\"".($this->a_Config["CHAR_NUMBER"] + 1)."\">&nbsp;";
            if($allowRefresh) $s .= "<input type=\"button\" id=\"".AMDev_Captcha::DATA_CAPTCHA_REFRESH."\" ".(($this->a_Config["CAPTCHA_CLASS"]!=="")? "class=\"button_".$this->a_Config["CAPTCHA_CLASS"]."\" " : "")."value=\"".$this->a_Messages["MSG_BUTTON_REFRESH"]."\" onClick=\"".$this->GetXajaxCallbackScript()."\" /><br />";

			return $s;
		}
		// *** end DisplayCaptchaFormPart() ***
		
		// *** string GetXajaxCallbackScript() ***
		private function GetXajaxCallbackScript()
		{
			return $this->a_XajaxCallback->getScript();
		}
		// *** end GetXajaxCallbackScript() ***
		
		// *** string SendToHost() ***
		private function SendToHost($host, $path, $data, $method="POST", $port=80, $referer="", $useragent="")
		{
			$buffer = "";
			$host = preg_replace("@^http://@i", "", $host);
			$socket = fsockopen($host, $port, $errno, $errstr);
			$referer = ($referer === "")? $_SERVER["SCRIPT_NAME"] : $referer;
			$referer = "http://".$host.$this->SanitizePath($referer, false, true);
			
			if($socket)
			{
				// Method is case-sensitive
				$method = strtoupper($method);
				
				// Strip ? sign from data if present
				$data = (substr($data, 0, 1) === "?")? substr($data, 0) : $data;
				
				if($method === "GET") $path .= "?".$data;

				$headers = $method." ".$path." HTTP/1.1\r\n";
				$headers .= "Host: ".$host."\r\n";
				$headers .= "Referer: ".$referer."\r\n";
				$headers .= "Content-type: application/x-www-form-urlencoded\r\n";
				$headers .= "Content-length: ".strlen($data)."\r\n";
				if($useragent !== "") $headers .= "User-Agent: MSIE\r\n";
				$headers .= "Connection: close\r\n\r\n";
				if ($method == "POST") $headers .= $data;
				
				fwrite($socket, $headers);
				if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: HTTP headers sent: (\r\n<br /><textarea style=\"width: 500px; height=200px;\">".htmlspecialchars($headers)."</textarea>\r\n<br />)";

				while(!feof($socket))
				{
					$buffer .= fgets($socket, 4096);
				}
				
				fclose($socket);
			}
			else
			{
				if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Redirection failed. Error (".$errno.": ".$errstr.")";
			}
			return $buffer;
		}
		// *** end SendToHost() ***
		
		// *** string GetLocationRedirect() ***
		private function GetLocationRedirect($response_data)
		{
			if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Trying to get redirect url from response: (\r\n<br /><textarea style=\"width: 500px; height=200px;\">".htmlspecialchars($response_data)."</textarea>\r\n<br />)";
			
			$redirectUrl = "";
			$pos = strpos($response_data, "\n\n");
			if ($pos === false)
				$pos = strpos($response_data, "\r\n\r\n");
	
			if ($pos === false)
				return;
	
			$headers = substr($response_data, 0, $pos);
			$data = substr($response_data, $pos + 1);
			$tok_headers = strtok($headers, "\n");
			while ($tok_headers !== false)
			{
				if(ereg("HTTP/1.1 30(1|2|3|5|7)", trim($tok_headers)))
				{
					while ($tok_headers !== false)
					{
						if(preg_match("/Location: (.*)$/", $line = trim($tok_headers), $matches))
						{
							$redirectUrl = chop($matches[1]);
						}
						$tok_headers = strtok("\n");
					}
				}
				else if(ereg("HTTP/1.1 100 Continue", trim($tok_headers)))
				{
					$pos = strpos($data, "\n\n");
					if ($pos === false) {
						$pos = strpos($data, "\r\n\r\n");
					}
			
					if ($pos === false) {
						// BAD DATA
						return;
					}
					$headers = substr($data, 0, $pos);
					$data = substr($data, $pos + 1);
					$tok_headers = strtok($headers, "\n");
				}
				
				$tok_headers = strtok("\n");
			}
			
			return $redirectUrl;;
		}
		// *** end GetLocationRedirect ***
		
		// *** mixed[bool|integer] GCFileCounter() ***
		private function GCFileCounter($filename, $increase=false, $setFixValue=false)
		{
			if((is_file($filename)? true : touch($filename)))
			{
				if(is_readable($filename) && is_writable($filename))
				{
					$fp = @fopen($filename, "r");
					if($fp)
					{
						$counter = (int)trim(fgets($fp));
						fclose($fp);

						if($increase)
						{
							if($setFixValue !== false)
							{
								$counter = (int)$setFixValue;
							}
							else
							{
								$counter++;
							}
							$fp = @fopen($filename, "w");
							if($fp)
							{
								fputs($fp, $counter);
								fclose($fp);
								return $counter;
							}
							else return false;
						}
						else
						{
							return $counter;
						}
					}
					else return false;
				}
				else return false;
			}
			else return false;
		}
		// *** end GCFileCounter ***
		
		// *** bool GCCollect() ***
		private function GCCollect()
		{
			$ret = false;
			$captchas = 0;
			$trashed = 0;
			
			if($handle = @opendir($this->a_Config["CAPTCHA_PATH"]))
			{
				$ret = true;
				while(false !== ($file = readdir($handle)))
				{
					if(is_file($this->a_Config["CAPTCHA_PATH"].$file))
					{
						// Check for name prefix, extension and filetime
						if(substr($file, 0, strlen($this->a_Config["CAPTCHA_PREFIX"])) == $this->a_Config["CAPTCHA_PREFIX"])
						{
							if(strrchr($file, ".") === ".jpg")
							{
								$captchas++;
								if((time() - filemtime($this->a_Config["CAPTCHA_PATH"].$file)) >= $this->a_Config["GC_CAPTCHA_PERIOD"])
								{
									$trashed++;
									$res = @unlink($this->a_Config["CAPTCHA_PATH"].$file);
									if(!$res) $ret = false;
								}
							}
						}
					}
				}
				closedir($handle);
			}
			if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br>-Captcha-Debug: There are (".$captchas.") captcha images in (".$this->a_Config["CAPTCHA_PATH"]."), where (".$trashed.") where trashed";

			return $ret;
		}
		// *** end GCCollect() ***
		
		// *** mixed[bool|integer] CreateGCCounterFile() ***
		private function CreateGCCounterFile($filename)
		{
			$counter = 0;
			$fp = @fopen($filename, "w");
			if($fp)
			{
				fputs($fp, $counter);
				fclose($fp);
				return $counter;
			}
			else return false;
		}
		// *** end CreateGCCounterFile() ***
		
		// *** array GetFilePerms() ***
		private function GetFilePerms($file) {
			$perms = fileperms($file);
			
			$perm_array = array(
					"TYPE" => "",
					"OWNER" => array(
							"READ" => false,
							"WRITE" => false,
							"EXEC" => false
								),
					"GROUP" => array(
							"READ" => false,
							"WRITE" => false,
							"EXEC" => false
								),
					"WORLD" => array(
							"READ" => false,
							"WRITE" => false,
							"EXEC" => false
								),
					"STRING" => ""
					);
			
			if(($perms & 0xC000) == 0xC000)
			{
				// Socket
				$info = "s";
				$perm_array["TYPE"] = "Socket";
			}
			elseif(($perms & 0xA000) == 0xA000)
			{
				// Symbolic Link
				$info = "l";
				$perm_array["TYPE"] = "Symbolic Link";
			}
			elseif(($perms & 0x8000) == 0x8000)
			{
				// Regular
				$info = "-";
				$perm_array["TYPE"] = "Regular";
			}
			elseif(($perms & 0x6000) == 0x6000)
			{
				// Block special
				$info = "b";
				$perm_array["TYPE"] = "Block special";
			}
			elseif(($perms & 0x4000) == 0x4000)
			{
				// Directory
				$info = "d";
				$perm_array["TYPE"] = "Directory";
			}
			elseif(($perms & 0x2000) == 0x2000)
			{
				// Character special
				$info = "c";
				$perm_array["TYPE"] = "Character special";
			}
			elseif(($perms & 0x1000) == 0x1000)
			{
				// FIFO pipe
				$info = "p";
				$perm_array["TYPE"] = "FIFO pipe";
			}
			else
			{
				// Unknown
				$info = "?";
				$perm_array["TYPE"] = "Unknown";
			}
			
			// Owner
			$info .= (($perm_array["OWNER"]["READ"] = ($perms & 0x0100)) ? "r" : "-");
			$info .= (($perm_array["OWNER"]["WRITE"] = ($perms & 0x0080)) ? "w" : "-");
			$info .= (($perms & 0x0040) ?
			   (($perm_array["OWNER"]["EXEC"] = ($perms & 0x0800)) ? "s" : "x" ) :
			   (($perm_array["OWNER"]["EXEC"] = ($perms & 0x0800)) ? "S" : "-"));
			// Group
			$info .= (($perm_array["GROUP"]["READ"] = ($perms & 0x0020)) ? "r" : "-");
			$info .= (($perm_array["GROUP"]["WRITE"] = ($perms & 0x0010)) ? "w" : "-");
			$info .= (($perms & 0x0008) ?
				  (($perm_array["GROUP"]["EXEC"] = ($perms & 0x0400)) ? "s" : "x" ) :
				  (($perm_array["GROUP"]["EXEC"] = ($perms & 0x0400)) ? "S" : "-"));
			// World
			$info .= (($perm_array["WORLD"]["READ"] = ($perms & 0x0004)) ? "r" : "-");
			$info .= (($perm_array["WORLD"]["WRITE"] = ($perms & 0x0002)) ? "w" : "-");
			$info .= (($perms & 0x0001) ?
			   (($perm_array["WORLD"]["EXEC"] = ($perms & 0x0200)) ? "t" : "x" ) :
			   (($perm_array["WORLD"]["EXEC"] = ($perms & 0x0200)) ? "T" : "-"));
			
			$perm_array["STRING"] = $info;
			
			return $perm_array;
		}
		// *** end GetFilePerms() ***
	
	// === end Private Functions ===
	
	// === Public Methods ===
	
		// *** void SanitizeFormData() ***
		public function SanitizeFormData()
		{
			if(isset($_GET) && is_array($_GET))
			{
				while(list($key, $value) = each($_GET))
					$_GET["$key"] = strip_tags($value);
			}
			if(isset($_POST) && is_array($_POST))
			{
				while(list($key, $value) = each($_POST))
					$_POST["$key"] = strip_tags($value);
			}
			if(isset($_COOKIES) && is_array($_COOKIES))
			{
				while(list($key, $value) = each($_COOKIES))
					$_COOKIES["$key"] = strip_tags($value);
			}
		}
		// *** end SanitizeFormData() ***
		
		// *** void RedirectPost() ***
		public function RedirectPost($url)
		{
			$arr = array();
			$host = $_SERVER["HTTP_HOST"];
			$path = $this->ConvertToAbsoluteUrlPath($url, false);
			$data = "";
			
			echo "<br />Path: ".$path;
			
			if($this->a_Config["FORM_METHOD"] === "POST" && isset($_POST) && is_array($_POST))
				$arr = $_POST;
			elseif($this->a_Config["FORM_METHOD"] === "POST" && isset($_POST) && is_array($_POST))
				$arr = $_GET;
			
			if(isset($arr) && is_array($arr))
			{
				// Strip all captcha data
				foreach($arr as $key=>$value)
				{
					if($key !== AMDev_Captcha::DATA_PUBLIC_KEY && $key !== AMDev_Captcha::DATA_PRIVATE_KEY && $key !== AMDev_Captcha::DATA_SESSION_INFO && $key !== AMDev_Captcha::DATA_CAPTCHA_REFRESH)
						$data .= "&".$key."=".$value;
				}
				$data = substr($data, 1);
			}
			
			$result = $this->GetLocationRedirect($this->SendToHost($host, $path, $data, $this->a_Config["FORM_METHOD"]));
			if($this->a_Config["DEBUG_ENABLED"])
			{
				echo "\r\n<br>-Captcha-Debug: Redirecting to (<a href=\"".$result."\" target=\"_blank\">".$result."</a>) (as this is debug mode it will not redirect automatically, click the supplied link to simulate the redirection in a new page)";
			}
			else
			{
				header("Location: ".$result);
			}
		}
		// *** end RedirectPost() ***
		
		// *** void StartBuffering() ***
		public function StartBuffering()
		{
			ob_start();
		}
		// *** end StartBuffering() ***
		
		// *** void EndBuffering() ***
		public function EndBuffering()
		{
			ob_end_flush();
		}
		// *** end EndBuffering() ***
		
	// === end Public Methods
	
	// === Public Functions ===
	
		// *** string DisplayFormPart() ***
		public function DisplayFormPart()
		{
			$current_try = $this->GetTryCount(false);
            if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Generate a string which contains current try: (".$current_try.")";
			
            $s  = "<div id=\"".AMDev_Captcha::DATA_CAPTCHA_BLOCK."\" name=\"".AMDev_Captcha::DATA_CAPTCHA_BLOCK."\" ".(($this->a_Config["CAPTCHA_CLASS"]!=="")? "class=\"".$this->a_Config["CAPTCHA_CLASS"]."\"" : "").">";
            $s .= $this->DisplayCaptchaFormPart($current_try);
            $s .= "</div>";
			if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Output FormPart with captcha-image.<br /><br />";
			
            return $s;
		}
		// *** end DisplayFormPart() ***
		
		// *** int ValidateCaptcha() ***
		public function ValidateCaptcha()
        {
            if($this->CheckCaptcha($this->a_PublicKey, $this->a_PrivateKey))
            {
				// Private Key is valid
                if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Validating submitted form returns: (1: Private Key is valid.)";
                return 1;
            }
            else
            {
                if($this->a_CurrentTry > $this->a_Config["MAX_TRIES"])
                {
					// Private Key is not valid and maximum tries already reached
                    if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Validating submitted form returns: (3: Private Key is invalid. No more tries left.)";
                    return 3;
                }
                elseif($this->a_CurrentTry > 1)		// Bug fixed
                {
					// Private Key is not valid but maximum tries is not reached
                    if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Validating submitted form returns: (2: Private Key is invalid. Try again.)";
                    return 2;
                }
                else
                {
					// The Captcha is displayed for the first time
                    if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Validating submitted form returns: (0: Captcha displayed for the first time.)";
                    return 0;
                }
            }
        }
		// *** end ValidateCaptcha() ***
		
		// *** [xajaxResponse|string] DisplayCaptchaFormPart() ***
		public function RefreshCaptcha($publicKey, $sessionInfo)
		{
			$imgPath = realpath($this->a_Config["CAPTCHA_PATH"]).$this->GetImageFilename($publicKey);
			
            // If file exists erase it from disk as it was already displayed once
            if(file_exists($imgPath))
            {
                $res = @unlink($imgPath)? "true" : "false";
                if($this->a_Config["DEBUG_ENABLED"]) echo "\r\n<br />-Captcha-Debug: Delete image (".$imgPath.") returns: ($res)";
			}
			
			$current_try = $this->GetTryCount(false, $sessionInfo);
			$s = $this->DisplayCaptchaFormPart($current_try);
			
			if($this->a_XajaxLoaded)
			{
				$objResponse = new xajaxResponse();
				$objResponse->assign(AMDev_Captcha::DATA_CAPTCHA_BLOCK, "innerHTML", $s);
				
				return $objResponse;
			}
			else
			{
				return $s;
			}
		}
		// *** end DisplayCaptchaFormPart() ***
		
		// *** string DisplayErrorMsg() ***
		public function DisplayErrorMsg($errKey)
		{
			if($errKey !== "")
				if(array_key_exists($errKey, $this->a_Errors))
					return $this->a_Errors[$errKey];
			return "";
		}
		// *** end DisplayErrorMsg() ***

		// *** ConfigToString() ***
		public function ConfigToString()
		{
			$str = "===== CAPTCHA CONFIG =====";
			$str .= "\r\r\n<br />DEBUGGING: ".$this->a_Config["DEBUG_ENABLED"];
			foreach($this->a_Config as $key=>$val)
			{
				if(is_array($val))
				{
					$str .= "\r\r\n<br />".$key." = ";
					$s = "";
					foreach($val as $k=>$v)
					{
						$s .= $v.", ";
					}
					$str .= substr($s, 0, strlen($s) - 2);
				}
				else
				{
					$str .= "\r\r\n<br />".$key." = ".$val;
				}
			}
			
			return $str;
		}
		// *** end ConfigToString() ***
		
		// *** string GetXajaxScript() ***
		public function GetXajaxScript()
		{
			return $this->a_Xajax->getJavascript();
		}
		// *** end GetXajaxScript() ***
		
		// *** string GetMessage() ***
		public function GetMessage($key="")
		{
			$ret = "";
			if($key !== "" && array_key_exists($key, $this->a_Messages))
			{
				$ret = $this->a_Messages[$key];
			}
			
			return $ret;
		}
		// *** end GetXajaxScript() ***
	
	// === end Public Functions ===
}
// @@@ end AMDev_Captcha @@@

/**
  * PHP-Class AMDev_LanguageFileParser Version 1.0, released 29-10-2007
  *
  * Author: Axel Pardemann, hide@address.com
  *
  * Download: 
  *
  * License: GNU LGPL (http://www.opensource.org/licenses/lgpl-license.html)
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
  * License as published by the Free Software Foundation; either
  * version 2.1 of the License, or (at your option) any later version.
  *
  * This library is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  * Lesser General Public License for more details.
  *
  * Notes:
  * - Language based on ini files
  *
  **/

// @@@ class AMDev_LanguageFileParser @@@
class AMDev_LanguageFileParser
{
	// === Attributes ===
	
		// *** Private Attributes ***
		
		private $a_ParsedArray = NULL;
		
		// *** end Private Attributes ***
		
	// === end Attributes ===
	
	// === Constructor ===
	function AMDev_LanguageFileParser()
	{
	}
	// === end Constructor
	
	// === Public Functions ===
		
		// *** bool ParseFile() ***
		public function ParseFile($filename)
		{
			if($this->a_ParsedArray = @parse_ini_file($filename, true))
				return true;
			else
				return false;
		}
		// *** end ParseFile() ***
		
		// *** array[] GetSection() ***
		public function GetSection($key)
		{
			if(!isset($this->a_ParsedArray))
				return NULL;
			else
				return $this->a_ParsedArray[$key];
		}
		// *** end GetSection() ***

		// *** string GetValue() ***      
		function getValue($section, $key)
		{
			if(!isset($this->a_ParsedArray))
				return NULL;
			else
			{
				if(!isset($this->a_ParsedArray[$section]))
					return false;
				else
					return $this->a_ParsedArray[$section][$key];
			}
		}
		// *** end GetValue() ***
	
		// *** mixed[array|string] Get() ***
		function Get($section, $key=NULL)
		{
			if(!isset($this->a_ParsedArray))
				return NULL;
			else
			{
				if(is_null($key))
					return $this->GetSection($section);
				else
					return $this->GetValue($section, $key);
			}
		}
		// *** end Get() ***
		
	// === end Public Functions ===
}
// @@@ end AMDev_LanguageFileParser @@@
?>
Return current item: AMDev_Captcha