2 /***************************************************************/
3 /* PhpCaptcha - A visual and audio CAPTCHA generation library
5 Software License Agreement (BSD License)
7 Copyright (C) 2005-2006, Edward Eliot.
10 Redistribution and use in source and binary forms, with or without
11 modification, are permitted provided that the following conditions are met:
13 * Redistributions of source code must retain the above copyright
14 notice, this list of conditions and the following disclaimer.
15 * Redistributions in binary form must reproduce the above copyright
16 notice, this list of conditions and the following disclaimer in the
17 documentation and/or other materials provided with the distribution.
18 * Neither the name of Edward Eliot nor the names of its contributors
19 may be used to endorse or promote products derived from this software
20 without specific prior written permission of Edward Eliot.
22 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" AND ANY
23 EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
24 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY
26 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
27 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
29 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
31 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 Last Updated: 18th April 2006 */
34 /***************************************************************/
36 /************************ Documentation ************************/
39 Documentation is available at http://www.ejeliot.com/pages/2
42 /************************ Default Options **********************/
44 // start a PHP session - this class uses sessions to store the generated
45 // code. Comment out if you are calling already from your application
48 // class defaults - change to effect globally
50 define('CAPTCHA_SESSION_ID', 'php_captcha');
51 define('CAPTCHA_WIDTH', 200); // max 500
52 define('CAPTCHA_HEIGHT', 50); // max 200
53 define('CAPTCHA_NUM_CHARS', 5);
54 define('CAPTCHA_NUM_LINES', 70);
55 define('CAPTCHA_CHAR_SHADOW', false);
56 define('CAPTCHA_OWNER_TEXT', '');
57 define('CAPTCHA_CHAR_SET', ''); // defaults to A-Z
58 define('CAPTCHA_CASE_INSENSITIVE', true);
59 define('CAPTCHA_BACKGROUND_IMAGES', '');
60 define('CAPTCHA_MIN_FONT_SIZE', 16);
61 define('CAPTCHA_MAX_FONT_SIZE', 25);
62 define('CAPTCHA_USE_COLOUR', false);
63 define('CAPTCHA_FILE_TYPE', 'jpeg');
64 define('CAPTCHA_FLITE_PATH', '/usr/bin/flite');
65 define('CAPTCHA_AUDIO_PATH', '/tmp/'); // must be writeable by PHP process
67 /************************ End Default Options **********************/
69 // don't edit below this line (unless you want to change the class!)
82 var $bCaseInsensitive;
83 var $vBackgroundImages;
91 $aFonts, // array of TrueType fonts to use - specify full path
92 $iWidth = CAPTCHA_WIDTH
, // width of image
93 $iHeight = CAPTCHA_HEIGHT
// height of image
96 $this->aFonts
= $aFonts;
97 $this->SetNumChars(CAPTCHA_NUM_CHARS
);
98 $this->SetNumLines(CAPTCHA_NUM_LINES
);
99 $this->DisplayShadow(CAPTCHA_CHAR_SHADOW
);
100 $this->SetOwnerText(CAPTCHA_OWNER_TEXT
);
101 $this->SetCharSet(CAPTCHA_CHAR_SET
);
102 $this->CaseInsensitive(CAPTCHA_CASE_INSENSITIVE
);
103 $this->SetBackgroundImages(CAPTCHA_BACKGROUND_IMAGES
);
104 $this->SetMinFontSize(CAPTCHA_MIN_FONT_SIZE
);
105 $this->SetMaxFontSize(CAPTCHA_MAX_FONT_SIZE
);
106 $this->UseColour(CAPTCHA_USE_COLOUR
);
107 $this->SetFileType(CAPTCHA_FILE_TYPE
);
108 $this->SetWidth($iWidth);
109 $this->SetHeight($iHeight);
112 function CalculateSpacing() {
113 $this->iSpacing
= (int)($this->iWidth
/ $this->iNumChars
);
116 function SetWidth($iWidth) {
117 $this->iWidth
= $iWidth;
118 if ($this->iWidth
> 500) $this->iWidth
= 500; // to prevent perfomance impact
119 $this->CalculateSpacing();
122 function SetHeight($iHeight) {
123 $this->iHeight
= $iHeight;
124 if ($this->iHeight
> 200) $this->iHeight
= 200; // to prevent performance impact
127 function SetNumChars($iNumChars) {
128 $this->iNumChars
= $iNumChars;
129 $this->CalculateSpacing();
132 function SetNumLines($iNumLines) {
133 $this->iNumLines
= $iNumLines;
136 function DisplayShadow($bCharShadow) {
137 $this->bCharShadow
= $bCharShadow;
140 function SetOwnerText($sOwnerText) {
141 $this->sOwnerText
= $sOwnerText;
144 function SetCharSet($vCharSet) {
145 // check for input type
146 if (is_array($vCharSet)) {
147 $this->aCharSet
= $vCharSet;
149 if ($vCharSet != '') {
150 // split items on commas
151 $aCharSet = explode(',', $vCharSet);
154 $this->aCharSet
= array();
156 // loop through items
157 foreach ($aCharSet as $sCurrentItem) {
158 // a range should have 3 characters, otherwise is normal character
159 if (strlen($sCurrentItem) == 3) {
160 // split on range character
161 $aRange = explode('-', $sCurrentItem);
163 // check for valid range
164 if (count($aRange) == 2 && $aRange[0] < $aRange[1]) {
165 // create array of characters from range
166 $aRange = range($aRange[0], $aRange[1]);
168 // add to charset array
169 $this->aCharSet
= array_merge($this->aCharSet
, $aRange);
172 $this->aCharSet
[] = $sCurrentItem;
179 function CaseInsensitive($bCaseInsensitive) {
180 $this->bCaseInsensitive
= $bCaseInsensitive;
183 function SetBackgroundImages($vBackgroundImages) {
184 $this->vBackgroundImages
= $vBackgroundImages;
187 function SetMinFontSize($iMinFontSize) {
188 $this->iMinFontSize
= $iMinFontSize;
191 function SetMaxFontSize($iMaxFontSize) {
192 $this->iMaxFontSize
= $iMaxFontSize;
195 function UseColour($bUseColour) {
196 $this->bUseColour
= $bUseColour;
199 function SetFileType($sFileType) {
200 // check for valid file type
201 if (in_array($sFileType, array('gif', 'png', 'jpeg'))) {
202 $this->sFileType
= $sFileType;
204 $this->sFileType
= 'jpeg';
208 function DrawLines() {
209 for ($i = 0; $i < $this->iNumLines
; $i++
) {
211 if ($this->bUseColour
) {
212 $iLineColour = imagecolorallocate($this->oImage
, rand(100, 250), rand(100, 250), rand(100, 250));
214 $iRandColour = rand(100, 250);
215 $iLineColour = imagecolorallocate($this->oImage
, $iRandColour, $iRandColour, $iRandColour);
219 imageline($this->oImage
, rand(0, $this->iWidth
), rand(0, $this->iHeight
), rand(0, $this->iWidth
), rand(0, $this->iHeight
), $iLineColour);
223 function DrawOwnerText() {
224 // allocate owner text colour
225 $iBlack = imagecolorallocate($this->oImage
, 0, 0, 0);
226 // get height of selected font
227 $iOwnerTextHeight = imagefontheight(2);
228 // calculate overall height
229 $iLineHeight = $this->iHeight
- $iOwnerTextHeight - 4;
231 // draw line above text to separate from CAPTCHA
232 imageline($this->oImage
, 0, $iLineHeight, $this->iWidth
, $iLineHeight, $iBlack);
235 imagestring($this->oImage
, 2, 3, $this->iHeight
- $iOwnerTextHeight - 3, $this->sOwnerText
, $iBlack);
237 // reduce available height for drawing CAPTCHA
238 $this->iHeight
= $this->iHeight
- $iOwnerTextHeight - 5;
241 function GenerateCode() {
245 // loop through and generate the code letter by letter
246 for ($i = 0; $i < $this->iNumChars
; $i++
) {
247 if (count($this->aCharSet
) > 0) {
248 // select random character and add to code string
249 $this->sCode
.= $this->aCharSet
[array_rand($this->aCharSet
)];
251 // select random character and add to code string
252 $this->sCode
.= chr(rand(65, 90));
256 // save code in session variable
257 if ($this->bCaseInsensitive
) {
258 $_SESSION[CAPTCHA_SESSION_ID
] = strtoupper($this->sCode
);
260 $_SESSION[CAPTCHA_SESSION_ID
] = $this->sCode
;
264 function DrawCharacters() {
265 // loop through and write out selected number of characters
266 for ($i = 0; $i < strlen($this->sCode
); $i++
) {
267 // select random font
268 $sCurrentFont = $this->aFonts
[array_rand($this->aFonts
)];
270 // select random colour
271 if ($this->bUseColour
) {
272 $iTextColour = imagecolorallocate($this->oImage
, rand(0, 100), rand(0, 100), rand(0, 100));
274 if ($this->bCharShadow
) {
276 $iShadowColour = imagecolorallocate($this->oImage
, rand(0, 100), rand(0, 100), rand(0, 100));
279 $iRandColour = rand(0, 100);
280 $iTextColour = imagecolorallocate($this->oImage
, $iRandColour, $iRandColour, $iRandColour);
282 if ($this->bCharShadow
) {
284 $iRandColour = rand(0, 100);
285 $iShadowColour = imagecolorallocate($this->oImage
, $iRandColour, $iRandColour, $iRandColour);
289 // select random font size
290 $iFontSize = rand($this->iMinFontSize
, $this->iMaxFontSize
);
292 // select random angle
293 $iAngle = rand(-30, 30);
295 // get dimensions of character in selected font and text size
296 $aCharDetails = imageftbbox($iFontSize, $iAngle, $sCurrentFont, $this->sCode
[$i], array());
298 // calculate character starting coordinates
299 $iX = $this->iSpacing
/ 4 +
$i * $this->iSpacing
;
300 $iCharHeight = $aCharDetails[2] - $aCharDetails[5];
301 $iY = $this->iHeight
/ 2 +
$iCharHeight / 4;
303 // write text to image
304 imagefttext($this->oImage
, $iFontSize, $iAngle, $iX, $iY, $iTextColour, $sCurrentFont, $this->sCode
[$i], array());
306 if ($this->bCharShadow
) {
307 $iOffsetAngle = rand(-30, 30);
309 $iRandOffsetX = rand(-5, 5);
310 $iRandOffsetY = rand(-5, 5);
312 imagefttext($this->oImage
, $iFontSize, $iOffsetAngle, $iX +
$iRandOffsetX, $iY +
$iRandOffsetY, $iShadowColour, $sCurrentFont, $this->sCode
[$i], array());
317 function WriteFile($sFilename) {
318 if ($sFilename == '') {
319 // tell browser that data is jpeg
320 header("Content-type: image/$this->sFileType");
323 switch ($this->sFileType
) {
325 $sFilename != '' ?
imagegif($this->oImage
, $sFilename) : imagegif($this->oImage
);
328 $sFilename != '' ?
imagepng($this->oImage
, $sFilename) : imagepng($this->oImage
);
331 $sFilename != '' ?
imagejpeg($this->oImage
, $sFilename) : imagejpeg($this->oImage
);
335 function Create($sFilename = '') {
336 // check for required gd functions
337 if (!function_exists('imagecreate') ||
!function_exists("image$this->sFileType") ||
($this->vBackgroundImages
!= '' && !function_exists('imagecreatetruecolor'))) {
341 // get background image if specified and copy to CAPTCHA
342 if (is_array($this->vBackgroundImages
) ||
$this->vBackgroundImages
!= '') {
344 $this->oImage
= imagecreatetruecolor($this->iWidth
, $this->iHeight
);
346 // create background image
347 if (is_array($this->vBackgroundImages
)) {
348 $iRandImage = array_rand($this->vBackgroundImages
);
349 $oBackgroundImage = imagecreatefromjpeg($this->vBackgroundImages
[$iRandImage]);
351 $oBackgroundImage = imagecreatefromjpeg($this->vBackgroundImages
);
354 // copy background image
355 imagecopy($this->oImage
, $oBackgroundImage, 0, 0, 0, 0, $this->iWidth
, $this->iHeight
);
357 // free memory used to create background image
358 imagedestroy($oBackgroundImage);
361 $this->oImage
= imagecreate($this->iWidth
, $this->iHeight
);
364 // allocate white background colour
365 imagecolorallocate($this->oImage
, 255, 255, 255);
367 // check for owner text
368 if ($this->sOwnerText
!= '') {
369 $this->DrawOwnerText();
372 // check for background image before drawing lines
373 if (!is_array($this->vBackgroundImages
) && $this->vBackgroundImages
== '') {
377 $this->GenerateCode();
378 $this->DrawCharacters();
380 // write out image to file or browser
381 $this->WriteFile($sFilename);
383 // free memory used in creating image
384 imagedestroy($this->oImage
);
389 // call this method statically
390 function Validate($sUserCode, $bCaseInsensitive = true) {
391 if ($bCaseInsensitive) {
392 $sUserCode = strtoupper($sUserCode);
395 if (!empty($_SESSION[CAPTCHA_SESSION_ID
]) && $sUserCode == $_SESSION[CAPTCHA_SESSION_ID
]) {
396 // clear to prevent re-use
397 unset($_SESSION[CAPTCHA_SESSION_ID
]);
406 // this class will only work correctly if a visual CAPTCHA has been created first using PhpCaptcha
407 class AudioPhpCaptcha
{
412 function AudioPhpCaptcha(
413 $sFlitePath = CAPTCHA_FLITE_PATH
, // path to flite binary
414 $sAudioPath = CAPTCHA_AUDIO_PATH
// the location to temporarily store the generated audio CAPTCHA
416 $this->SetFlitePath($sFlitePath);
417 $this->SetAudioPath($sAudioPath);
419 // retrieve code if already set by previous instance of visual PhpCaptcha
420 if (isset($_SESSION[CAPTCHA_SESSION_ID
])) {
421 $this->sCode
= $_SESSION[CAPTCHA_SESSION_ID
];
425 function SetFlitePath($sFlitePath) {
426 $this->sFlitePath
= $sFlitePath;
429 function SetAudioPath($sAudioPath) {
430 $this->sAudioPath
= $sAudioPath;
433 function Mask($sText) {
434 $iLength = strlen($sText);
436 // loop through characters in code and format
437 $sFormattedText = '';
438 for ($i = 0; $i < $iLength; $i++
) {
439 // comma separate all but first and last characters
440 if ($i > 0 && $i < $iLength - 1) {
441 $sFormattedText .= ', ';
442 } elseif ($i == $iLength - 1) { // precede last character with "and"
443 $sFormattedText .= ' and ';
445 $sFormattedText .= $sText[$i];
449 "The %1\$s characters are as follows: %2\$s",
450 "%2\$s, are the %1\$s letters",
451 "Here are the %1\$s characters: %2\$s",
452 "%1\$s characters are: %2\$s",
453 "%1\$s letters: %2\$s"
456 $iPhrase = array_rand($aPhrases);
458 return sprintf($aPhrases[$iPhrase], $iLength, $sFormattedText);
462 $sText = $this->Mask($this->sCode
);
463 $sFile = md5($this->sCode
.time());
465 // create file with flite
466 shell_exec("$this->sFlitePath -t \"$sText\" -o $this->sAudioPath$sFile.wav");
469 header('Content-type: audio/x-wav');
470 header("Content-Disposition: attachment;filename=$sFile.wav");
473 echo file_get_contents("$this->sAudioPath$sFile.wav");
475 // delete temporary file
476 @unlink
("$this->sAudioPath$sFile.wav");
481 class PhpCaptchaColour
extends PhpCaptcha
{
482 function PhpCaptchaColour($aFonts, $iWidth = CAPTCHA_WIDTH
, $iHeight = CAPTCHA_HEIGHT
) {
483 // call parent constructor
484 parent
::PhpCaptcha($aFonts, $iWidth, $iHeight);
487 $this->UseColour(true);