Follow-up to bug #13428: pi1/captcha.php: PHP Fatal Error when called directly
[TYPO3CMS/Extensions/sr_freecap.git] / pi1 / class.tx_srfreecap_pi1.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2005-2011 Stanislas Rolland <typo3(arobas)sjbr.ca>
6 * All rights reserved
7 *
8 * This script is part of the TYPO3 project. The TYPO3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 * A copy is found in the textfile GPL.txt and important notices to the license
17 * from the author is found in LICENSE.txt distributed with these scripts.
18 *
19 *
20 * This script is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
24 *
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
27 /**
28 * Integrates freeCap v1.4.1 into TYPO3 and generates the freeCap CAPTCHA image.
29 *
30 *
31 * @author Stanislas Rolland <typo3(arobas)sjbr.ca>
32 */
33 /************************************************************\
34 *
35 * freeCap v1.4.1 Copyright 2005 Howard Yeend
36 * www.puremango.co.uk
37 *
38 * This file is part of freeCap.
39 *
40 * freeCap is free software; you can redistribute it and/or modify
41 * it under the terms of the GNU General Public License as published by
42 * the Free Software Foundation; either version 2 of the License, or
43 * (at your option) any later version.
44 *
45 * freeCap is distributed in the hope that it will be useful,
46 * but WITHOUT ANY WARRANTY; without even the implied warranty of
47 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
48 * GNU General Public License for more details.
49 *
50 * You should have received a copy of the GNU General Public License
51 * along with freeCap; if not, write to the Free Software
52 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
53 *
54 *
55 \************************************************************/
56 require_once(PATH_tslib.'class.tslib_pibase.php');
57
58 class tx_srfreecap_pi1 extends tslib_pibase {
59 var $cObj; // The backReference to the mother cObj object set at call time
60 var $prefixId = 'tx_srfreecap_pi1'; // Same as class name
61 var $scriptRelPath = 'pi1/class.tx_srfreecap_pi1.php'; // Path to this script relative to the extension dir.
62 var $extKey = 'sr_freecap'; // The extension key.
63 var $extPrefix = 'tx_srfreecap';
64 var $conf = array();
65
66 function main($conf) {
67 $this->conf = $conf;
68 $this->pi_loadLL();
69
70 // Get session data
71 $this->sessionData = $GLOBALS['TSFE']->fe_user->getKey('ses','tx_'.$this->extKey);
72
73 //////////////////////////////////////////////////////
74 ////// User Defined Vars:
75 //////////////////////////////////////////////////////
76
77 // try to avoid the 'free p*rn' method of CAPTCHA circumvention
78 // see www.wikipedia.com/captcha for more info
79 $this->site_tags[0] = "To avoid spam, please do NOT enter the text if";
80 $this->site_tags[1] = "this site is not example.org";
81 // or more simply:
82 //$site_tags[0] = "for use only on example.org";
83 // reword or add lines as you please
84 // or if you don't want any text:
85 $this->site_tags = $this->conf['siteTag'] ? explode('|', sprintf($this->pi_getLL('site_tag'), (isset($this->conf['siteTagDomain']) ? $this->conf['siteTagDomain'] : 'example.org'))) : null;
86 // where to write the above:
87 // 0=top
88 // 1=bottom
89 // 2=both
90 $this->tag_pos = isset($this->conf['siteTagPosition']) ? $this->conf['siteTagPosition'] : 1;
91
92 // which type of hash to use?
93 // possible values: "sha1", "md5", "crc32"
94 // sha1 supported by PHP4.3.0+
95 // md5 supported by PHP3+
96 // crc32 supported by PHP4.0.1+
97 // store in session so can validate in form processor
98 $this->sessionData[$this->extKey . '_hash_func'] = 'md5';
99
100 // image type:
101 // possible values: "jpg", "png", "gif"
102 // jpg doesn't support transparency (transparent bg option ends up white)
103 // png isn't supported by old browsers (see http://www.libpng.org/pub/png/pngstatus.html)
104 // gif may not be supported by your GD Lib.
105 $this->output = $this->conf['imageFormat'] ? $this->conf['imageFormat'] : 'png';
106
107 // true = generate pseudo-random string, false = use dictionary
108 // dictionary is easier to recognise
109 // - both for humans and computers, so use random string if you're paranoid.
110 $this->use_dict = $this->conf['useWordsList'] ? true : false;
111
112 // if your server is NOT set up to deny web access to files beginning ".ht"
113 // then you should ensure the dictionary file is kept outside the web directory
114 // eg: if www.foo.com/index.html points to c:\website\www\index.html
115 // then the dictionary should be placed in c:\website\dict.txt
116 // test your server's config by trying to access the dictionary through a web browser
117 // you should NOT be able to view the contents.
118 // can leave this blank if not using dictionary
119 if (!trim($this->conf['defaultWordsList'])) {
120 $this->conf['defaultWordsList'] = 'EXT:sr_freecap/res/words/.ht_default_freecap_words';
121 }
122 if (is_file(dirname(t3lib_div::getFileAbsFileName($this->conf['defaultWordsList'])) . '/.ht_' . $this->LLkey . '_freecap_words')) {
123 $this->dict_location = dirname(t3lib_div::getFileAbsFileName($this->conf['defaultWordsList'])) . '/.ht_' . $this->LLkey . '_freecap_words';
124 } elseif (is_file(dirname(t3lib_div::getFileAbsFileName($this->conf['defaultWordsList'])) . '/.ht_default_freecap_words')) {
125 $this->dict_location = dirname(t3lib_div::getFileAbsFileName($this->conf['defaultWordsList'])) . '/.ht_default_freecap_words';
126 }
127
128 // used to calculate image width, and for non-dictionary word generation
129 $this->max_word_length = $this->conf['maxWordLength'] ? $this->conf['maxWordLength'] : 6;
130
131 // text colour
132 // 0=one random colour for all letters
133 // 1=different random colour for each letter
134 if ($this->conf['textColor'] == '0') {
135 $this->col_type = 0;
136 } else {
137 $this->col_type = 1;
138 }
139
140 // maximum times a user can refresh the image
141 // on a 6500 word dictionary, I think 15-50 is enough to not annoy users and make BF unfeasble.
142 // further notes re: BF attacks in "avoid brute force attacks" section, below
143 // on the other hand, those attempting OCR will find the ability to request new images
144 // very useful; if they can't crack one, just grab an easier target...
145 // for the ultra-paranoid, setting it to <5 will still work for most users
146 $this->max_attempts = $this->conf['maxAttempts'] ? $this->conf['maxAttempts'] : 50;
147
148 // list of fonts to use
149 // font size should be around 35 pixels wide for each character.
150 // you can use my GD fontmaker script at www.puremango.co.uk to create your own fonts
151 // There are other programs to can create GD fonts, but my script allows a greater
152 // degree of control over exactly how wide each character is, and is therefore
153 // recommended for 'special' uses. For normal use of GD fonts,
154 // the GDFontGenerator @ http://www.philiplb.de is excellent for convering ttf to GD
155 // the fonts included with freeCap *only* include lowercase alphabetic characters
156 // so are not suitable for most other uses
157 // to increase security, you really should add other fonts
158 if ($this->conf['generateNumbers']) {
159 $this->font_locations = Array('EXT:' . $this->extKey . '/res/fonts/.anonymous.gdf');
160 } else {
161 $this->font_locations = Array(
162 'EXT:' . $this->extKey . '/res/fonts/.ht_freecap_font1.gdf',
163 'EXT:' . $this->extKey . '/res/fonts/.ht_freecap_font2.gdf',
164 'EXT:' . $this->extKey . '/res/fonts/.ht_freecap_font3.gdf',
165 'EXT:' . $this->extKey . '/res/fonts/.ht_freecap_font4.gdf',
166 'EXT:' . $this->extKey . '/res/fonts/.ht_freecap_font5.gdf'
167 );
168 }
169 if ($this->conf['fontFiles']) {
170 $this->font_locations = t3lib_div::trimExplode(',', $this->conf['fontFiles'], 1);
171 }
172 for ($i = 0; $i < sizeof($this->font_locations); $i++) {
173 if (substr($this->font_locations[$i],0,4)=='EXT:') {
174 $this->font_locations[$i] = t3lib_div::getFileAbsFileName($this->font_locations[$i]);
175 } else {
176 $this->font_locations[$i] = PATH_site.'uploads/'.$this->extPrefix.'/'.$this->font_locations[$i];
177 }
178 }
179
180 // background:
181 // 0=transparent (if jpg, white)
182 // 1=white bg with grid
183 // 2=white bg with squiggles
184 // 3=morphed image blocks
185 // 'random' background from v1.3 didn't provide any extra security (according to 2 independent experts)
186 // many thanks to http://ocr-research.org.ua and http://sam.zoy.org/pwntcha/ for testing
187 // for jpgs, 'transparent' is white
188 switch ($this->conf['backgroundType']) {
189 case 'Transparent':
190 $this->bg_type = 0;
191 break;
192 case 'White with grid':
193 $this->bg_type = 1;
194 break;
195 case 'White with squiggles':
196 $this->bg_type = 2;
197 break;
198 case 'Morphed image blocks':
199 $this->bg_type = 3;
200 break;
201 default:
202 $this->bg_type = 2;
203 break;
204 }
205
206 // text position X
207 $this->textHorizontalPosition = $this->conf['textHorizontalPosition'] ? intval($this->conf['textHorizontalPosition']) : 32;
208
209 // text position Y
210 $this->textVerticalPosition = $this->conf['textVerticalPosition'] ? intval($this->conf['textVerticalPosition']) : 15;
211
212 // text morh factor
213 $this->morphFactor = $this->conf['morphFactor'] ? $this->conf['morphFactor'] : 1;
214
215 // Limits for text colour
216 if (isset($this->conf['colorMaximumDarkness'])) {
217 $this->colorMaximumDarkness = intval($this->conf['colorMaximumDarkness']);
218 }
219 if (isset($this->conf['colorMaximumLightness'])) {
220 $this->colorMaximumLightness = intval($this->conf['colorMaximumLightness']);
221 }
222
223 // should we blur the background? (looks nicer, makes text easier to read, takes longer)
224 $this->blur_bg = $this->conf['backgroundBlur'] ? true : false;
225
226 // for bg_type 3, which images should we use?
227 // if you add your own, make sure they're fairly 'busy' images (ie a lot of shapes in them)
228 $this->bg_images = Array(
229 'EXT:sr_freecap/res/images/.ht_freecap_im1.jpg',
230 'EXT:sr_freecap/res/images/.ht_freecap_im2.jpg',
231 'EXT:sr_freecap/res/images/.ht_freecap_im3.jpg',
232 'EXT:sr_freecap/res/images/.ht_freecap_im4.jpg',
233 'EXT:sr_freecap/res/images/.ht_freecap_im5.jpg'
234 );
235
236 // for non-transparent backgrounds only:
237 // if 0, merges CAPTCHA with bg
238 // if 1, write CAPTCHA over bg
239 $this->merge_type = $this->conf['mergeWithBackground'] ? 0 : 1;
240
241 // should we morph the bg? (recommend yes, but takes a little longer to compute)
242 $this->morph_bg = $this->conf['backgroundMorph'] ? true : false;
243
244 // you shouldn't need to edit anything below this, but it's extensively commented if you do want to play
245 // have fun, and email me with ideas, or improvements to the code (very interested in speed improvements)
246 // hope this script saves some spam :-)
247
248 // seed random number generator
249 // PHP 4.2.0+ doesn't need this, but lower versions will
250 $this->seed_func($this->make_seed());
251
252 // read each font and get font character widths
253 $this->font_widths = Array();
254 for ($i=0 ; $i < sizeof($this->font_locations); $i++) {
255 $handle = fopen($this->font_locations[$i],"r");
256 // read header of GD font, up to char width
257 $c_wid = fread($handle,12);
258 $this->font_widths[$i] = ord($c_wid{8})+ord($c_wid{9})+ord($c_wid{10})+ord($c_wid{11});
259 fclose($handle);
260 }
261 // modify image width depending on maximum possible length of word
262 // you shouldn't need to use words > 6 chars in length really.
263 $width = ($this->max_word_length*(array_sum($this->font_widths)/sizeof($this->font_widths))) + (isset($this->conf['imageAdditionalWidth']) ? intval($this->conf['imageAdditionalWidth']) : 75);
264 $height = $this->conf['imageHeight'] ? $this->conf['imageHeight'] : 90;
265
266 $this->im = ImageCreate($width, $height);
267 $this->im2 = ImageCreate($width, $height);
268
269 //////////////////////////////////////////////////////
270 ////// Avoid Brute Force Attacks:
271 //////////////////////////////////////////////////////
272
273 if (empty($this->sessionData[$this->extKey . '_attempts'])) {
274 $this->sessionData[$this->extKey . '_attempts'] = 1;
275 } else {
276 $this->sessionData[$this->extKey . '_attempts']++;
277
278 // if more than ($this->max_attempts) refreshes, block further refreshes
279 // can be negated by connecting with new session id
280 // could get round this by storing num attempts in database against IP
281 // could get round that by connecting with different IP (eg, using proxy servers)
282 // in short, there's little point trying to avoid brute forcing
283 // the best way to protect against BF attacks is to ensure the dictionary is not
284 // accessible via the web or use random string option
285 if ($this->sessionData[$this->extKey . '_attempts'] > $this->max_attempts) {
286 $this->sessionData[$this->extKey . '_word_hash'] = false;
287 $this->sessionData[$this->extKey . '_word_accessible'] = false;
288 $this->sessionData[$this->extKey . '_hash_func'] = false;
289 $GLOBALS['TSFE']->fe_user->setKey('ses','tx_'.$this->extKey,$this->sessionData);
290 $GLOBALS['TSFE']->storeSessionData();
291 $string = $this->pi_getLL('max_attempts');
292 $font = 5;
293 $width = imagefontwidth($font) * strlen($string);
294 $height = imagefontheight($font);
295 $this->im3 = ImageCreate($width+2, $height+20);
296 $bg = ImageColorAllocate($this->im3,255,255,255);
297 ImageColorTransparent($this->im3,$bg);
298 $red = ImageColorAllocate($this->im3, 255, 0, 0);
299 ImageString($this->im3,$font,1,10,$string,$red);
300 $this->sendImage($this->im3);
301 exit();
302 }
303 }
304
305 // get word
306 $word = $this->getWord();
307
308 // save hash of word for comparison
309 // using hash so that if there's an insecurity elsewhere (eg on the form processor),
310 // an attacker could only get the hash
311 // also, shared servers usually give all users access to the session files
312 // echo `ls /tmp`; and echo `more /tmp/someone_elses_session_file`; usually work
313 // so even if your site is 100% secure, someone else's site on your server might not be
314 // hence, even if attackers can read the session file, they can't get the freeCap word
315 // (though most hashes are easy to brute force for simple strings)
316 $this->sessionData[$this->extKey . '_word_hash'] = $this->hash_func($word);
317
318 // We use a simple encrypt to prevent the session from being exposed
319 if ($this->conf['accessibleOutput'] && in_array('mcrypt', get_loaded_extensions())) {
320 $code = 'accessiblemustbe007';
321 $cyph = $this->easy_crypt($word, $code);
322 $this->sessionData[$this->extKey . '_word_accessible'] = $cyph;
323 }
324
325 // Store the session data
326 $GLOBALS['TSFE']->fe_user->setKey('ses','tx_'.$this->extKey,$this->sessionData);
327 $GLOBALS['TSFE']->storeSessionData();
328
329 // Output image
330 $this->buildImage($word, $width, $height);
331
332 $this->sendImage($this->im);
333
334 // unset all sensetive vars
335 // in case someone include()s this file on a shared server
336 // you might think this unneccessary, as it exit()s
337 // but by using register_shutdown_function
338 // on a -very- insecure shared server, they -might- be able to get the word
339 unset($word);
340
341 exit();
342 }
343
344 // functions to call for random number generation
345 // mt_rand produces 'better' random numbers
346 // but if your server doesn't support it, it's fine to use rand instead
347 //$this->rand_func = "mt_rand";
348 //$this->seed_func = "mt_srand";
349 function rand_func ($min, $max) {
350 if ($min > $max) {
351 $newMin = $max;
352 $newMax = $min;
353 } else {
354 $newMin = $min;
355 $newMax = $max;
356 }
357 return mt_rand($newMin, $newMax);
358 }
359
360 function seed_func($seed) {
361 return mt_srand($seed);
362 }
363
364 function make_seed() {
365 // from http://php.net/srand
366 list($usec, $sec) = explode(' ', microtime());
367 return (float) $sec + ((float) $usec * 100000);
368 }
369
370 function rand_color() {
371 if($this->bg_type==3) {
372 // needs darker colour..
373 $colorMaximumDarkness = isset($this->colorMaximumDarkness) ? $this->colorMaximumDarkness : 10;
374 $colorMaximumLightness = isset($this->colorMaximumLightness) ? $this->colorMaximumLightness : 100;
375 } else {
376 $colorMaximumDarkness = isset($this->colorMaximumDarkness) ? $this->colorMaximumDarkness : 30;
377 $colorMaximumLightness = isset($this->colorMaximumLightness) ? $this->colorMaximumLightness : 140;
378 }
379 return $this->rand_func($colorMaximumDarkness, $colorMaximumLightness);
380 }
381
382 function hash_func($string) {
383 return md5($string);
384 }
385
386 function myImageBlur($im) {
387 // w00t. my very own blur function
388 // in GD2, there's a gaussian blur function. bunch of bloody show-offs... :-)
389
390 $width = imagesx($im);
391 $height = imagesy($im);
392
393 $temp_im = ImageCreateTrueColor($width,$height);
394 $bg = ImageColorAllocate($temp_im,150,150,150);
395
396 // preserves transparency if in orig image
397 ImageColorTransparent($temp_im,$bg);
398
399 // fill bg
400 ImageFill($temp_im,0,0,$bg);
401
402 // anything higher than 3 makes it totally unreadable
403 // might be useful in a 'real' blur function, though (ie blurring pictures not text)
404 $distance = 1;
405 // use $distance=30 to have multiple copies of the word. not sure if this is useful.
406
407 // blur by merging with itself at different x/y offsets:
408 ImageCopyMerge($temp_im, $im, 0, 0, 0, $distance, $width, $height-$distance, 70);
409 ImageCopyMerge($im, $temp_im, 0, 0, $distance, 0, $width-$distance, $height, 70);
410 ImageCopyMerge($temp_im, $im, 0, $distance, 0, 0, $width, $height, 70);
411 ImageCopyMerge($im, $temp_im, $distance, 0, 0, 0, $width, $height, 70);
412 // remove temp image
413 ImageDestroy($temp_im);
414
415 return $im;
416 }
417
418 function sendImage($pic) {
419 // output image with appropriate headers
420 header(base64_decode("WC1DYXB0Y2hhOiBmcmVlQ2FwIDEuNCAtIHd3dy5wdXJlbWFuZ28uY28udWs="));
421 header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
422 switch($this->output) {
423 // add other cases as desired
424 case "jpg":
425 header("Content-Type: image/jpeg");
426 ImageJPEG($pic);
427 break;
428 case "gif":
429 header("Content-Type: image/gif");
430 ImageGIF($pic);
431 break;
432 case "png":
433 default:
434 header("Content-Type: image/png");
435 ImagePNG($pic);
436 break;
437 }
438 // kill GD images (removes from memory)
439 ImageDestroy($this->im);
440 ImageDestroy($this->im2);
441 if(!empty($this->im3)) {
442 ImageDestroy($this->im3);
443 }
444 // the below aren't really essential, but might aid an OCR attack if discovered.
445 // so we unset them
446 unset($this->use_dict);
447 unset($this->dict_location);
448 unset($this->max_word_length);
449 unset($this->bg_type);
450 unset($this->bg_images);
451 unset($this->merge_type);
452 unset($this->morph_bg);
453 unset($this->col_type);
454 unset($this->max_attempts);
455 unset($this->font_locations);
456 }
457
458 function getWord() {
459
460 // get word
461 if($this->use_dict) {
462 // load dictionary and choose random word
463 // keep dictionary in non-web accessible folder, or htaccess it
464 // or modify so word comes from a database; SELECT word FROM words ORDER BY rand() LIMIT 1
465 // took 0.11 seconds when 'words' had 10,000 records
466
467 $words = @file($this->dict_location);
468
469 $word = strtolower($words[$this->rand_func(0,sizeof($words)-1)]);
470 // cut off line breaks
471 $word = preg_replace('/['.preg_quote(chr(10).chr(13)).']+/', '', $word);
472 // might be large file so forget it now (frees memory)
473 $words = "";
474 unset($words);
475 } else {
476 // based on code originally by breakzero at hotmail dot com
477 // (http://uk.php.net/manual/en/function.rand.php)
478 // generate pseudo-random string
479 // doesn't use ijtf as are easily mistaken
480
481 // I'm not using numbers because the custom fonts I've created don't support anything other than
482 // lowercase or space (but you can download new fonts or create your own using my GD fontmaker script)
483
484 if ($this->conf['generateNumbers']) {
485 $consonants = '123456789';
486 $vowels = '123456789';
487 } else {
488 $consonants = 'bcdghklmnpqrsvwxyz';
489 $vowels = 'aeuo';
490 }
491 $word = "";
492
493 $wordlen = $this->rand_func(5,$this->max_word_length);
494 for($i=0 ; $i<$wordlen ; $i++) {
495 // don't allow to start with 'vowel'
496 if($this->rand_func(0,4)>=2 && $i!=0) {
497 $word .= $vowels{$this->rand_func(0,strlen($vowels)-1)};
498 } else {
499 $word .= $consonants{$this->rand_func(0,strlen($consonants)-1)};
500 }
501 }
502 }
503 return $word;
504 }
505
506 function buildImage($word, $width, $height) {
507
508 // how faded should the bg be? (100=totally gone, 0=bright as the day)
509 // to test how much protection the bg noise gives, take a screenshot of the freeCap image
510 // and take it into a photo editor. play with contrast and brightness.
511 // If you can remove most of the bg, then it's not a good enough percentage
512 switch($this->bg_type) {
513 case 0:
514 break;
515 case 1:
516 case 2:
517 $bg_fade_pct = 65;
518 break;
519 case 3:
520 $bg_fade_pct = 50;
521 break;
522 }
523 // slightly randomise the bg fade
524 $bg_fade_pct += $this->rand_func(-2,2);
525
526 //////////////////////////////////////////////////////
527 ////// Fill BGs and Allocate Colours:
528 //////////////////////////////////////////////////////
529
530 // set tag colour
531 // have to do this before any distortion
532 // (otherwise colour allocation fails when bg type is 1)
533 $tag_col = ImageColorAllocate($this->im,10,10,10);
534 $site_tag_col2 = ImageColorAllocate($this->im2,0,0,0);
535
536 // set debug colours (text colours are set later)
537 $debug = ImageColorAllocate($this->im, 255, 0, 0);
538 $debug2 = ImageColorAllocate($this->im2, 255, 0, 0);
539
540 // set background colour (can change to any colour not in possible $text_col range)
541 // it doesn't matter as it'll be transparent or coloured over.
542 // if you're using bg_type 3, you might want to try to ensure that the color chosen
543 // below doesn't appear too much in any of your background images.
544 $bg = ImageColorAllocate($this->im, 254, 254, 254);
545 $bg2 = ImageColorAllocate($this->im2, 254, 254, 254);
546
547 // set transparencies
548 ImageColorTransparent($this->im,$bg);
549 // im2 transparent to allow characters to overlap slightly while morphing
550 ImageColorTransparent($this->im2,$bg2);
551
552 // fill backgrounds
553 ImageFill($this->im,0,0,$bg);
554 ImageFill($this->im2,0,0,$bg2);
555
556 if($this->bg_type!=0) {
557 // generate noisy background, to be merged with CAPTCHA later
558 // any suggestions on how best to do this much appreciated
559 // sample code would be even better!
560 // I'm not an OCR expert (hell, I'm not even an image expert; puremango.co.uk was designed in MsPaint)
561 // so the noise models are based around my -guesswork- as to what would make it hard for an OCR prog
562 // ideally, the character obfuscation would be strong enough not to need additional background noise
563 // in any case, I hope at least one of the options given here provide some extra security!
564
565 $this->im3 = ImageCreateTrueColor($width,$height);
566 $temp_bg = ImageCreateTrueColor($width*1.5,$height*1.5);
567 $bg3 = ImageColorAllocate($this->im3,255,255,255);
568 ImageFill($this->im3,0,0,$bg3);
569 $temp_bg_col = ImageColorAllocate($temp_bg,255,255,255);
570 ImageFill($temp_bg,0,0,$temp_bg_col);
571
572 // we draw all noise onto temp_bg
573 // then if we're morphing, merge from temp_bg to im3
574 // or if not, just copy a $widthx$height portion of $temp_bg to $im3
575 // temp_bg is much larger so that when morphing, the edges retain the noise.
576
577 if($this->bg_type==1) {
578 // grid bg:
579
580 // draw grid on x
581 for($i=$this->rand_func(6,20) ; $i<$width*2 ; $i+=$this->rand_func(10,25)) {
582 ImageSetThickness($temp_bg,$this->rand_func(2,6));
583 $text_r = $this->rand_func(100,150);
584 $text_g = $this->rand_func(100,150);
585 $text_b = $this->rand_func(100,150);
586 $text_colour3 = ImageColorAllocate($temp_bg, $text_r, $text_g, $text_b);
587
588 ImageLine($temp_bg,$i,0,$i,$height*2,$text_colour3);
589 }
590 // draw grid on y
591 for($i=$this->rand_func(6,20) ; $i<$height*2 ; $i+=$this->rand_func(10,25)) {
592 ImageSetThickness($temp_bg,$this->rand_func(2,6));
593 $text_r = $this->rand_func(100,150);
594 $text_g = $this->rand_func(100,150);
595 $text_b = $this->rand_func(100,150);
596 $text_colour3 = ImageColorAllocate($temp_bg, $text_r, $text_g, $text_b);
597
598 ImageLine($temp_bg,0,$i,$width*2, $i ,$text_colour3);
599 }
600 } else if($this->bg_type==2) {
601 // draw squiggles!
602
603 $bg3 = ImageColorAllocate($this->im3,255,255,255);
604 ImageFill($this->im3,0,0,$bg3);
605 ImageSetThickness($temp_bg,4);
606
607 for($i=0 ; $i<strlen($word)+1 ; $i++) {
608 $text_r = $this->rand_func(100,150);
609 $text_g = $this->rand_func(100,150);
610 $text_b = $this->rand_func(100,150);
611 $text_colour3 = ImageColorAllocate($temp_bg, $text_r, $text_g, $text_b);
612
613 $points = Array();
614 // draw random squiggle for each character
615 // the longer the loop, the more complex the squiggle
616 // keep random so OCR can't say "if found shape has 10 points, ignore it"
617 // each squiggle will, however, be a closed shape, so OCR could try to find
618 // line terminations and start from there. (I don't think they're that advanced yet..)
619 for($j=1 ; $j<$this->rand_func(5,10) ; $j++) {
620 $points[] = $this->rand_func(1*(20*($i+1)),1*(50*($i+1)));
621 $points[] = $this->rand_func(30,$height+30);
622 }
623
624 ImagePolygon($temp_bg,$points,intval(sizeof($points)/2),$text_colour3);
625 }
626
627 } else if($this->bg_type==3) {
628 // take random chunks of $this->bg_images and paste them onto the background
629
630 for($i=0 ; $i<sizeof($this->bg_images) ; $i++) {
631 // read each image and its size
632 $temp_im[$i] = ImageCreateFromJPEG(t3lib_div::getFileAbsFileName($this->bg_images[$i]));
633 $temp_width[$i] = imagesx($temp_im[$i]);
634 $temp_height[$i] = imagesy($temp_im[$i]);
635 }
636
637 $blocksize = $this->rand_func(20,60);
638 for($i=0 ; $i<$width*2 ; $i+=$blocksize) {
639 // could randomise blocksize here... hardly matters
640 for($j=0 ; $j<$height*2 ; $j+=$blocksize) {
641 $image_index = $this->rand_func(0,sizeof($temp_im)-1);
642 $cut_x = $this->rand_func(0,$temp_width[$image_index]-$blocksize);
643 $cut_y = $this->rand_func(0,$temp_height[$image_index]-$blocksize);
644 ImageCopy($temp_bg, $temp_im[$image_index], $i, $j, $cut_x, $cut_y, $blocksize, $blocksize);
645 }
646 }
647 for($i=0 ; $i<sizeof($temp_im) ; $i++) {
648 // remove bgs from memory
649 ImageDestroy($temp_im[$i]);
650 }
651
652 // for debug:
653 //sendImage($temp_bg);
654 }
655
656 // for debug:
657 //sendImage($this->im3);
658
659 if($this->morph_bg) {
660 // morph background
661 // we do this separately to the main text morph because:
662 // a) the main text morph is done char-by-char, this is done across whole image
663 // b) if an attacker could un-morph the bg, it would un-morph the CAPTCHA
664 // hence bg is morphed differently to text
665 // why do we morph it at all? it might make it harder for an attacker to remove the background
666 // morph_chunk 1 looks better but takes longer
667 // this is a different and less perfect morph than the one we do on the CAPTCHA
668 // occasonally you get some dark background showing through around the edges
669 // it doesn't need to be perfect as it's only the bg.
670 $morph_chunk = $this->rand_func(1,5);
671 $morph_y = 0;
672 for($x=0 ; $x<$width ; $x+=$morph_chunk) {
673 $morph_chunk = $this->rand_func(1,5);
674 $morph_y += $this->rand_func(-1,1);
675 ImageCopy($this->im3, $temp_bg, $x, 0, $x+30, 30+$morph_y, $morph_chunk, $height*2);
676 }
677
678 ImageCopy($temp_bg, $this->im3, 0, 0, 0, 0, $width, $height);
679
680 $morph_x = 0;
681 for($y=0 ; $y<=$height; $y+=$morph_chunk) {
682 $morph_chunk = $this->rand_func(1,5);
683 $morph_x += $this->rand_func(-1,1);
684 ImageCopy($this->im3, $temp_bg, $morph_x, $y, 0, $y, $width, $morph_chunk);
685
686 }
687 } else {
688 // just copy temp_bg onto im3
689 ImageCopy($this->im3,$temp_bg,0,0,30,30,$width,$height);
690 }
691
692 ImageDestroy($temp_bg);
693
694 if($this->blur_bg) {
695 $this->myImageBlur($this->im3);
696 }
697 }
698
699 //////////////////////////////////////////////////////
700 ////// Write Word
701 //////////////////////////////////////////////////////
702
703 // write word in random starting X position
704 $word_start_x = $this->rand_func(5, $this->textHorizontalPosition);
705 // y positions jiggled about later
706 $word_start_y = $this->textVerticalPosition;
707
708 if($this->col_type==0) {
709 $text_r = $this->rand_color();
710 $text_g = $this->rand_color();
711 $text_b = $this->rand_color();
712 $text_colour2 = ImageColorAllocate($this->im2, $text_r, $text_g, $text_b);
713 }
714
715 // write each char in different font
716 for($i=0 ; $i<strlen($word) ; $i++) {
717 if($this->col_type==1) {
718 $text_r = $this->rand_color();
719 $text_g = $this->rand_color();
720 $text_b = $this->rand_color();
721 $text_colour2 = ImageColorAllocate($this->im2, $text_r, $text_g, $text_b);
722 }
723
724 $j = $this->rand_func(0,sizeof($this->font_locations)-1);
725 $font = ImageLoadFont($this->font_locations[$j]);
726 ImageString($this->im2, $font, $word_start_x+($this->font_widths[$j]*$i), $word_start_y, $word{$i}, $text_colour2);
727 }
728 // use last pixelwidth
729 $font_pixelwidth = $this->font_widths[$j];
730
731 // for debug:
732 //sendImage($this->im2);
733
734 //////////////////////////////////////////////////////
735 ////// Morph Image:
736 //////////////////////////////////////////////////////
737
738 // calculate how big the text is in pixels
739 // (so we only morph what we need to)
740 $word_pix_size = $word_start_x+(strlen($word)*$font_pixelwidth);
741
742 // firstly move each character up or down a bit:
743 for($i=$word_start_x ; $i<$word_pix_size ; $i+=$font_pixelwidth) {
744 // move on Y axis
745 // deviates at least 4 pixels between each letter
746 $prev_y = $y_pos;
747 do{
748 $y_pos = $this->rand_func(-5,5);
749 } while($y_pos<$prev_y+2 && $y_pos>$prev_y-2);
750 ImageCopy($this->im, $this->im2, $i, $y_pos, $i, 0, $font_pixelwidth, $height);
751
752 // for debug:
753 //ImageRectangle($this->im,$i,$y_pos+10,$i+$font_pixelwidth,$y_pos+70,$debug);
754 }
755
756 // for debug:
757 //sendImage($this->im);
758
759 ImageFilledRectangle($this->im2,0,0,$width,$height,$bg2);
760
761 // randomly morph each character individually on x-axis
762 // this is where the main distortion happens
763 // massively improved since v1.2
764 $y_chunk = 1;
765 $morph_factor = $this->morphFactor;
766 $morph_x = 0;
767 for($j=0 ; $j<strlen($word) ; $j++) {
768 $y_pos = 0;
769 for($i=0 ; $i<=$height; $i+=$y_chunk) {
770 $orig_x = $word_start_x+($j*$font_pixelwidth);
771 // morph x += so that instead of deviating from orig x each time, we deviate from where we last deviated to
772 // get it? instead of a zig zag, we get more of a sine wave.
773 // I wish we could deviate more but it looks crap if we do.
774 $morph_x += $this->rand_func(-$morph_factor,$morph_factor);
775 // had to change this to ImageCopyMerge when starting using ImageCreateTrueColor
776 // according to the manual; "when (pct is) 100 this function behaves identically to imagecopy()"
777 // but this is NOT true when dealing with transparencies...
778 ImageCopyMerge($this->im2, $this->im, $orig_x+$morph_x, $i+$y_pos, $orig_x, $i, $font_pixelwidth, $y_chunk, 100);
779
780 // for debug:
781 //ImageLine($this->im2, $orig_x+$morph_x, $i, $orig_x+$morph_x+1, $i+$y_chunk, $debug2);
782 //ImageLine($this->im2, $orig_x+$morph_x+$font_pixelwidth, $i, $orig_x+$morph_x+$font_pixelwidth+1, $i+$y_chunk, $debug2);
783 }
784 }
785
786 // for debug:
787 //sendImage($this->im2);
788
789 ImageFilledRectangle($this->im,0,0,$width,$height,$bg);
790 // now do the same on the y-axis
791 // (much easier because we can just do it across the whole image, don't have to do it char-by-char)
792 $y_pos = 0;
793 $x_chunk = 1;
794 for($i=0 ; $i<=$width ; $i+=$x_chunk) {
795 // can result in image going too far off on Y-axis;
796 // not much I can do about that, apart from make image bigger
797 // again, I wish I could do 1.5 pixels
798 $y_pos += $this->rand_func(-1,1);
799 ImageCopy($this->im, $this->im2, $i, $y_pos, $i, 0, $x_chunk, $height);
800
801 // for debug:
802 //ImageLine($this->im,$i+$x_chunk,0,$i+$x_chunk,100,$debug);
803 //ImageLine($this->im,$i,$y_pos+25,$i+$x_chunk,$y_pos+25,$debug);
804 }
805
806 // for debug:
807 //sendImage($this->im);
808
809 // blur edges:
810 // doesn't really add any security, but looks a lot nicer, and renders text a little easier to read
811 // for humans (hopefully not for OCRs, but if you know better, feel free to disable this function)
812 // (and if you do, let me know why)
813 $this->myImageBlur($this->im);
814
815 // for debug:
816 //sendImage($this->im);
817
818 if($this->output!="jpg" && $this->bg_type==0) {
819 // make background transparent
820 ImageColorTransparent($this->im,$bg);
821 }
822
823 //////////////////////////////////////////////////////
824 ////// Try to avoid 'free p*rn' style CAPTCHA re-use
825 //////////////////////////////////////////////////////
826 // ('*'ed to stop my site coming up for certain keyword searches on google)
827 // can obscure CAPTCHA word in some cases..
828 // write site tags 'shining through' the morphed image
829 ImageFilledRectangle($this->im2,0,0,$width,$height,$bg2);
830 if(is_array($this->site_tags)) {
831 $font = 2;
832 $siteTagFontWidth = 6;
833 $siteTagFontHeight = 10;
834 for($i=0 ; $i<sizeof($this->site_tags) ; $i++) {
835 // ensure tags are centered
836 $tag_width = strlen($this->site_tags[$i])*$siteTagFontWidth+8;
837 // write tag is chosen position
838 if($this->tag_pos==0 || $this->tag_pos==2) {
839 // write at top
840 ImageString($this->im2, $font, intval($width/2)-intval($tag_width/2)+5, $siteTagFontHeight*$i, $this->site_tags[$i], $site_tag_col2);
841 }
842 if($this->tag_pos==1 || $this->tag_pos==2) {
843 // write at bottom
844 ImageString($this->im2, $font, intval($width/2)-intval($tag_width/2)+5, ($height-((sizeof($this->site_tags)*$siteTagFontHeight+4))+($i*$siteTagFontHeight)), $this->site_tags[$i], $site_tag_col2);
845 }
846 }
847 }
848 ImageCopyMerge($this->im2,$this->im,0,0,0,0,$width,$height,80);
849 ImageCopy($this->im,$this->im2,0,0,0,0,$width,$height);
850
851 //////////////////////////////////////////////////////
852 ////// Merge with obfuscated background
853 //////////////////////////////////////////////////////
854
855 if($this->bg_type!=0) {
856 // merge bg image with CAPTCHA image to create smooth background
857 // fade bg:
858 if ($this->bg_type!=3) {
859 $temp_im = ImageCreateTrueColor($width,$height);
860 $white = ImageColorAllocate($temp_im,255,255,255);
861 ImageFill($temp_im,0,0,$white);
862 ImageCopyMerge($this->im3,$temp_im,0,0,0,0,$width,$height,$bg_fade_pct);
863 // for debug:
864 //sendImage($this->im3);
865 ImageDestroy($temp_im);
866 $c_fade_pct = 50;
867 } else {
868 $c_fade_pct = $bg_fade_pct;
869 }
870
871 // captcha over bg:
872 // might want to not blur if using this method
873 // otherwise leaves white-ish border around each letter
874 if ($this->merge_type == 1) {
875 ImageCopyMerge($this->im3,$this->im,0,0,0,0,$width,$height,100);
876 ImageCopy($this->im,$this->im3,0,0,0,0,$width,$height);
877 } else {
878 // bg over captcha:
879 ImageCopyMerge($this->im,$this->im3,0,0,0,0,$width,$height,$c_fade_pct);
880 }
881 }
882 unset($bg_fade_pct);
883 // for debug:
884 //sendImage($this->im);
885 }
886
887 /**
888 * Returns the localized label of the LOCAL_LANG key, $key, in utf-8 for gd (in fact gd is limited to Latin2)
889 * Notice that for debugging purposes prefixes for the output values can be set with the internal vars ->LLtestPrefixAlt and ->LLtestPrefix
890 *
891 * @param string The key from the LOCAL_LANG array for which to return the value.
892 * @param string Alternative string to return IF no value is found set for the key, neither for the local language nor the default.
893 * @param boolean If true, the output label is passed through htmlspecialchars()
894 * @return string The value from LOCAL_LANG.
895 */
896 function pi_getLL($key,$alt='',$hsc=FALSE) {
897 if (isset($this->LOCAL_LANG[$this->LLkey][$key])) {
898 $word = $this->LOCAL_LANG[$this->LLkey][$key];
899 if ($this->LOCAL_LANG_charset[$this->LLkey][$key]) {
900 $word = $GLOBALS['TSFE']->csConv($word, $this->LOCAL_LANG_charset[$this->LLkey][$key], 'utf-8');
901 }
902 } elseif ($this->altLLkey && isset($this->LOCAL_LANG[$this->altLLkey][$key])) {
903 $word = $this->LOCAL_LANG[$this->altLLkey][$key];
904 if ($this->LOCAL_LANG_charset[$this->altLLkey][$key]) {
905 $word = $GLOBALS['TSFE']->csConv($word, $this->LOCAL_LANG_charset[$this->altLLkey][$key], 'utf-8');
906 }
907 } elseif (isset($this->LOCAL_LANG['default'][$key])) {
908 $word = $this->LOCAL_LANG['default'][$key];
909 } else {
910 $word = $this->LLtestPrefixAlt.$alt;
911 }
912 // Don't know why the label is twice encoded...
913 $word = utf8_decode($word);
914 $output = $this->LLtestPrefix.$word;
915 if ($hsc) $output = htmlspecialchars($output);
916 return $output;
917 }
918
919 /**
920 * Encodes a string.
921 * Returns an array with the string as the first element and the initialization vector as the second element
922 */
923 function easy_crypt($string, $key) {
924 // When using MCRYPT_RAND, remember to call srand() before mcrypt_create_iv() to initialize the random number generator;
925 // it is not seeded automatically like rand() is.
926 srand((double) microtime() * 1000000);
927 $iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_BLOWFISH, MCRYPT_MODE_CBC), MCRYPT_RAND);
928 $string = mcrypt_encrypt(MCRYPT_BLOWFISH, $key, $string, MCRYPT_MODE_CBC, $iv);
929 return array(base64_encode($string), base64_encode($iv));
930 }
931
932 /**
933 * Decodes a string
934 * The first argument is an array as returned by easy_encrypt()
935 */
936 function easy_decrypt($cyph_arr, $key){
937 return trim(mcrypt_decrypt(MCRYPT_BLOWFISH, $key, base64_decode($cyph_arr[0]), MCRYPT_MODE_CBC, base64_decode($cyph_arr[1])));
938 }
939
940 }
941
942 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/sr_freecap/pi1/class.tx_srfreecap_pi1.php']) {
943 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/sr_freecap/pi1/class.tx_srfreecap_pi1.php']);
944 }
945
946 ?>