changed git call from https to git readonly
[atutor.git] / mods / wiki / plugins / lib / captcha.php
1 <?php
2 /*
3    Does emit a CAPTCHA graphic and form fields, which allows to tell real
4    people from bots.
5    Though a textual description is generated as well, this sort of access
6    restriction will knock out visually impaired users, and frustrate all
7    others anyhow. Therefore this should only be used as last resort for
8    defending against spambots. Because of the readable text and the used
9    colorspaces this is a weak implementation, not completely OCR-secure.
10
11    captcha::form() will return a html string to be inserted into textarea/
12    [save] <forms> and alike. User input is veryfied with captcha::check().
13    You should leave the sample COLLEGE.ttf next to this script, else you
14    have to define the _FONT_DIR constant correctly. Use only one type.
15
16    Includes a sluggish workaround for Internet Explorer; but this script
17    must reside in a www-accessible directory then.
18
19    Public Domain, available via http://freshmeat.net/p/captchaphp
20 */
21
22
23 #-- config
24 define("EWIKI_FONT_DIR", dirname(__FILE__));  // which fonts to use
25 define("CAPTCHA_INVERSE", 0);                 // white or black(=1)
26 define("CAPTCHA_TIMEOUT", 5000);              // in seconds (=max 4 hours)
27
28
29 /* static - (you could instantiate it, but...) */
30 class captcha {
31
32
33    /* gets parameter from $_REQUEST[] array (POST vars) and so can
34       verify input, @returns boolean
35    */
36    function check() {
37       if (($hash = $_REQUEST["captcha_hash"])
38       and ($pw = trim($_REQUEST["captcha_input"]))) {
39          return((captcha::hash($pw)==$hash) || (captcha::hash($pw,-1)==$hash));
40       }
41    }
42
43    
44    /* yields <input> fields html string (no complete form), with captcha
45       image already embedded as data:-URI
46    */
47    function form($title="&rarr; retype that here", $more="<small>Enter the correct letters and numbers from the image into the text box. This small test serves as access restriction against malicious bots. Simply reload the page if this graphic is too hard to read.</small>") {
48       $pw = captcha::mkpass();
49       $hash = captcha::hash($pw);
50       $maxsize = (strpos("MSIE", $_SERVER["HTTP_USER_AGENT"]) ? 3000 : 6000);
51       @header("Vary: User-Agent");
52       $img = "data:image/jpeg;base64,"
53            . base64_encode(captcha::image($pw, 200, 60, CAPTCHA_INVERSE, $maxsize));
54       $alt = htmlentities(captcha::textual_riddle($pw));
55       $html = 
56         '<table border="0" summary="captcha input"><tr>'
57       . '<td><img name="captcha_image" id="captcha_image" src="'.$img. '" height="60" width="200" alt="'.$alt. '" /></td>'
58       . '<td>'.$title. '<br/><input name="captcha_hash" type="hidden" value="'.$hash. '" />'
59       . '<input name="captcha_input" type="text" size="7" maxlength="16" style="height:46px; font-size:34px; font-weight:450;" />'
60       . '</td><td>'.$more.'</td>'
61       . '</tr></table>';
62       #-- js/html fix if ("MSIE")
63       {
64          $base = "http://$_SERVER[SERVER_NAME]:$_SERVER[SERVER_PORT]/"
65                . substr(realpath(__FILE__), strlen($_SERVER["DOCUMENT_ROOT"]));
66          $html .= <<<END
67 <script language="Javascript"><!--
68 if (/Microsoft/.test(navigator.appName)) {
69    var img = document.captcha_image;
70    img.src = "$base?_ddu=" + img.src.substr(23);
71 } //--></script>
72 END;
73       }
74       $html = "<div class=\"captcha\">$html</div>";
75       return($html);
76    }
77
78
79    /* generates alternative (non-graphic), human-understandable
80       representation of the passphrase
81    */
82    function textual_riddle($phrase) {
83       $symbols0 = '"\'-/_:';
84       $symbols1 = array("\n,", "\n;", ";", "\n&", "\n-", ",", ",", "\nand then", "\nfollowed by", "\nand", "\nand not a\n\"".chr(65+rand(0,26))."\",\nbut");
85       $s = "Guess the letters and numbers\n(passphrase riddle)\n--\n";
86       for ($p=0; $p<strlen($phrase); $p++) {
87          $c = $phrase[$p];
88          $add = "";
89          #-- asis
90          if (!rand(0,3)) {
91             $i = $symbols0[rand(0,strlen($symbols0)-1)];
92             $add = "$i$c$i";
93          }
94          #-- letter
95          elseif ($c >= 'A') {
96             $type = ($c >= 'a' ? "small " : "");
97             do {
98                $n = rand(-3,3);
99                $c2 = chr((ord($c) & 0x5F) + $n);
100             }
101             while (($c2 < 'A') || ($c2 > 'Z'));
102             if ($n < 0) {
103                $n = -$n;
104                $add .= "$type'$c2' +$n letters";
105             }
106             else {
107                $add .= "$n chars before $type$c2";
108             }
109          }
110          #-- number
111          else {
112             $add = "???";
113             $n = (int) $c;
114             do {
115                do { $x = rand(1, 10); } while (!$x);
116                $op = rand(0,11);
117                if ($op <= 2) {
118                   $add = "($add * $x)"; $n *= $x;
119                }
120                elseif ($op == 3) {
121                   $x = 2 * rand(1,2);
122                   $add = "($add / $x)"; $n /= $x;
123                }
124                elseif ($sel % 2) {
125                   $add = "($add + $x)"; $n += $x;
126                }
127                else {
128                   $add = "($add - $x)"; $n -= $x;
129                }
130             }
131             while (rand(0,1));
132             $add .= " = $n";
133          }
134          $s .= "$add";
135          $s .= $symbols1[rand(0,count($symbols1)-1)] . "\n";
136       }
137       return($s);
138    }
139
140
141    /* returns jpeg file stream with unscannable letters encoded
142       in front of colorful disturbing background
143    */
144    function image($phrase, $width=200, $height=60, $inverse=0, $maxsize=0xFFFFF) {
145    
146       #-- initialize in-memory image with gd library
147       srand(microtime()*21017);
148       $img = imagecreatetruecolor($width, $height);
149       $R = $inverse ? 0xFF : 0x00;
150       imagefilledrectangle($img, 0,0, $width,$height, captcha::random_color($img, 222^$R, 255^$R));
151       $c1 = rand(150^$R, 185^$R);
152       $c2 = rand(195^$R, 230^$R);
153       
154       #-- configuration
155       $fonts = array(
156         // "COLLEGE.ttf",
157       );
158       $fonts += glob(EWIKI_FONT_DIR."/*.ttf");
159       
160       #-- encolour bg
161       $wd = 20;
162       $x = 0;
163       while ($x < $width) {
164          imagefilledrectangle($img, $x, 0, $x+=$wd, $height, captcha::random_color($img, 222^$R, 255^$R));
165          $wd += max(10, rand(0, 20) - 10);
166       }
167
168       #-- make interesting background I, lines
169       $wd = 4;
170       $w1 = 0;
171       $w2 = 0;
172       for ($x=0; $x<$width; $x+=(int)$wd) {
173          if ($x < $width) {   // verical
174             imageline($img, $x+$w1, 0, $x+$w2, $height-1, captcha::random_color($img,$c1,$c2));
175          }
176          if ($x < $height) {  // horizontally ("y")
177             imageline($img, 0, $x-$w2, $width-1, $x-$w1, captcha::random_color($img,$c1,$c2));
178          }
179          $wd += rand(0,8) - 4;
180          if ($wd < 1) { $wd = 2; }
181          $w1 += rand(0,8) - 4;
182          $w2 += rand(0,8) - 4;
183          if (($x > $height) && ($y > $height)) {
184             break;
185          }
186       }
187       
188       #-- more disturbing II, random letters
189       $limit = rand(30,90);
190       for ($n=0; $n<$limit; $n++) {
191          $letter = "";
192          do {
193             $letter .= chr(rand(31,125)); // random symbol
194          } while (rand(0,1));
195          $size = rand(5, $height/2);
196          $half = (int) ($size / 2);
197          $x = rand(-$half, $width+$half);
198          $y = rand(+$half, $height);
199          $rotation = rand(60, 300);
200          $c1  = captcha::random_color($img, 130^$R, 240^$R);
201          $font = $fonts[rand(0, count($fonts)-1)];
202          imagettftext($img, $size, $rotation, $x, $y, $c1, $font, $letter);
203       }
204
205       #-- add the real text to it
206       $len = strlen($phrase);
207       $w1 = 10;
208       $w2 = $width / ($len+1);
209       for ($p=0; $p<$len; $p++) {
210          $letter = $phrase[$p];
211          $size = rand(18, $height/2.2);
212          $half = (int) $size / 2;
213          $rotation = rand(-33, 33);
214          $y = rand($size+3, $height-3);
215          $x = $w1 + $w2*$p;
216          $w1 += rand(-$width/90, $width/40);  // @BUG: last char could be +30 pixel outside of image
217          $font = $fonts[rand(0, count($fonts)-1)];
218          $r=rand(30,99); $g=rand(30,99); $b=rand(30,99); // two colors for shadow
219          $c1  = imagecolorallocate($img, $r*1^$R, $g*1^$R, $b*1^$R);
220          $c2  = imagecolorallocate($img, $r*2^$R, $g*2^$R, $b*2^$R);
221          imagettftext($img, $size, $rotation, $x+1, $y, $c2, $font, $letter);
222          imagettftext($img, $size, $rotation, $x, $y-1, $c1, $font, $letter);
223       }
224
225       #-- let JFIF stream be generated
226       $quality = 67;
227       $s = array();
228       do {
229          ob_start(); ob_implicit_flush(0);
230          imagejpeg($img, "", (int)$quality);
231          $jpeg = ob_get_contents(); ob_end_clean();
232          $size = strlen($jpeg);
233          $s_debug[] = ((int)($quality*10)/10) . "%=$size";
234          $quality = $quality * ($maxsize/$size) * 0.93 - 1.7;  // -($quality/7.222)*
235       }
236       while (($size > $maxsize) && ($quality >= 16));
237       imagedestroy($img);
238 #print_r($s_debug);
239       return($jpeg);
240    }
241
242
243    /* helper code */
244    function random_color($img, $a,$b) {
245       return imagecolorallocate($img, rand($a,$b), rand($a,$b), rand($a,$b));
246    }
247
248
249    /* unreversable hash from passphrase, with time() slice encoded */
250    function hash($text, $dtime=0) {
251       $text = strtolower($text);
252       $pfix = (int) (time() / CAPTCHA_TIMEOUT) + $dtime;
253       return md5("captcha::$pfix:$text::".__FILE__.":$_SERVER[SERVER_NAME]:80");
254    }
255
256
257    /* makes string of random letters for embedding into image and for
258       encoding as hash, later verification
259    */
260    function mkpass() {
261       $s = "";
262       for ($n=0; $n<10; $n++) {
263          $s .= chr(rand(0, 255));
264       }
265       $s = base64_encode($s);   // base64-set, but filter out unwanted chars
266       $s = preg_replace("/[+\/=IG0ODQR]/i", "", $s);  // (depends on YOUR font)
267       $s = substr($s, 0, rand(5,7));
268       return($s);
269    }
270 }
271
272
273 #-- IE workaround
274 if (isset($_REQUEST["_ddu"])) {
275    header("Content-Type: image/jpeg");
276    die(base64_decode(substr($_REQUEST["_ddu"], 0)));
277 }
278
279
280 ?>