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