1<?php 2/** 3 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 4 * @author Andreas Gohr <andi@splitbrain.org> 5 */ 6 7// must be run within Dokuwiki 8if(!defined('DOKU_INC')) die(); 9if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/'); 10 11 12class helper_plugin_captcha extends DokuWiki_Plugin { 13 14 protected $field_in = 'plugin__captcha'; 15 protected $field_sec = 'plugin__captcha_secret'; 16 protected $field_hp = 'plugin__captcha_honeypot'; 17 18 /** 19 * Constructor. Initializes field names 20 */ 21 public function __construct() { 22 $this->field_in = md5($this->_fixedIdent().$this->field_in); 23 $this->field_sec = md5($this->_fixedIdent().$this->field_sec); 24 $this->field_hp = md5($this->_fixedIdent().$this->field_hp); 25 } 26 27 /** 28 * Check if the CAPTCHA should be used. Always check this before using the methods below. 29 * 30 * @return bool true when the CAPTCHA should be used 31 */ 32 public function isEnabled() { 33 if(!$this->getConf('forusers') && $_SERVER['REMOTE_USER']) return false; 34 return true; 35 } 36 37 /** 38 * Returns the HTML to display the CAPTCHA with the chosen method 39 */ 40 public function getHTML() { 41 global $ID; 42 43 $rand = (float) (rand(0, 10000)) / 10000; 44 if($this->getConf('mode') == 'math') { 45 $code = $this->_generateMATH($this->_fixedIdent(), $rand); 46 $code = $code[0]; 47 $text = $this->getLang('fillmath'); 48 } elseif($this->getConf('mode') == 'question') { 49 $text = $this->getConf('question'); 50 } else { 51 $code = $this->_generateCAPTCHA($this->_fixedIdent(), $rand); 52 $text = $this->getLang('fillcaptcha'); 53 } 54 $secret = $this->encrypt($rand); 55 56 $txtlen = $this->getConf('lettercount'); 57 58 $out = ''; 59 $out .= '<div id="plugin__captcha_wrapper">'; 60 $out .= '<input type="hidden" name="'.$this->field_sec.'" value="'.hsc($secret).'" />'; 61 $out .= '<label for="plugin__captcha">'.$text.'</label> '; 62 63 switch($this->getConf('mode')) { 64 case 'math': 65 case 'text': 66 $out .= $this->_obfuscateText($code); 67 break; 68 case 'js': 69 $out .= '<span id="plugin__captcha_code">'.$this->_obfuscateText($code).'</span>'; 70 break; 71 case 'image': 72 $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&id='.$ID.'" '. 73 ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> '; 74 break; 75 case 'audio': 76 $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&id='.$ID.'" '. 77 ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> '; 78 $out .= '<a href="'.DOKU_BASE.'lib/plugins/captcha/wav.php?secret='.rawurlencode($secret).'&id='.$ID.'"'. 79 ' class="JSnocheck" title="'.$this->getLang('soundlink').'">'; 80 $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/sound.png" width="16" height="16"'. 81 ' alt="'.$this->getLang('soundlink').'" /></a>'; 82 break; 83 case 'figlet': 84 require_once(dirname(__FILE__).'/figlet.php'); 85 $figlet = new phpFiglet(); 86 if($figlet->loadfont(dirname(__FILE__).'/figlet.flf')) { 87 $out .= '<pre>'; 88 $out .= rtrim($figlet->fetch($code)); 89 $out .= '</pre>'; 90 } else { 91 msg('Failed to load figlet.flf font file. CAPTCHA broken', -1); 92 } 93 break; 94 } 95 $out .= ' <input type="text" size="'.$txtlen.'" name="'.$this->field_in.'" class="edit" /> '; 96 97 // add honeypot field 98 $out .= '<label class="no">'.$this->getLang('honeypot').'<input type="text" name="'.$this->field_hp.'" /></label>'; 99 $out .= '</div>'; 100 return $out; 101 } 102 103 /** 104 * Checks if the the CAPTCHA was solved correctly 105 * 106 * @param bool $msg when true, an error will be signalled through the msg() method 107 * @return bool true when the answer was correct, otherwise false 108 */ 109 public function check($msg = true) { 110 global $INPUT; 111 112 $code = ''; 113 $field_sec = $INPUT->str($this->field_sec); 114 $field_in = $INPUT->str($this->field_in); 115 $field_hp = $INPUT->str($this->field_hp); 116 117 // reconstruct captcha from provided $field_sec 118 $rand = $this->decrypt($field_sec); 119 120 if($this->getConf('mode') == 'math') { 121 $code = $this->_generateMATH($this->_fixedIdent(), $rand); 122 $code = $code[1]; 123 } elseif($this->getConf('mode') == 'question') { 124 $code = $this->getConf('answer'); 125 } else { 126 $code = $this->_generateCAPTCHA($this->_fixedIdent(), $rand); 127 } 128 129 // compare values 130 if(!$field_sec || 131 !$field_in || 132 $rand === false || 133 utf8_strtolower($field_in) != utf8_strtolower($code) || 134 trim($field_hp) !== '' 135 ) { 136 if($msg) msg($this->getLang('testfailed'), -1); 137 return false; 138 } 139 return true; 140 } 141 142 /** 143 * Build a semi-secret fixed string identifying the current page and user 144 * 145 * This string is always the same for the current user when editing the same 146 * page revision, but only for one day. Editing a page before midnight and saving 147 * after midnight will result in a failed CAPTCHA once, but makes sure it can 148 * not be reused which is especially important for the registration form where the 149 * $ID usually won't change. 150 * 151 * @return string 152 */ 153 public function _fixedIdent() { 154 global $ID; 155 $lm = @filemtime(wikiFN($ID)); 156 $td = date('Y-m-d'); 157 return auth_browseruid(). 158 auth_cookiesalt(). 159 $ID.$lm.$td; 160 } 161 162 /** 163 * Adds random space characters within the given text 164 * 165 * Keeps subsequent numbers without spaces (for math problem) 166 * 167 * @param $text 168 * @return string 169 */ 170 protected function _obfuscateText($text) { 171 $new = ''; 172 173 $spaces = array( 174 "\r", 175 "\n", 176 "\r\n", 177 ' ', 178 "\xC2\xA0", // \u00A0 NO-BREAK SPACE 179 "\xE2\x80\x80", // \u2000 EN QUAD 180 "\xE2\x80\x81", // \u2001 EM QUAD 181 "\xE2\x80\x82", // \u2002 EN SPACE 182 // "\xE2\x80\x83", // \u2003 EM SPACE 183 "\xE2\x80\x84", // \u2004 THREE-PER-EM SPACE 184 "\xE2\x80\x85", // \u2005 FOUR-PER-EM SPACE 185 "\xE2\x80\x86", // \u2006 SIX-PER-EM SPACE 186 "\xE2\x80\x87", // \u2007 FIGURE SPACE 187 "\xE2\x80\x88", // \u2008 PUNCTUATION SPACE 188 "\xE2\x80\x89", // \u2009 THIN SPACE 189 "\xE2\x80\x8A", // \u200A HAIR SPACE 190 "\xE2\x80\xAF", // \u202F NARROW NO-BREAK SPACE 191 "\xE2\x81\x9F", // \u205F MEDIUM MATHEMATICAL SPACE 192 193 "\xE1\xA0\x8E\r\n", // \u180E MONGOLIAN VOWEL SEPARATOR 194 "\xE2\x80\x8B\r\n", // \u200B ZERO WIDTH SPACE 195 "\xEF\xBB\xBF\r\n", // \uFEFF ZERO WIDTH NO-BREAK SPACE 196 ); 197 198 $len = strlen($text); 199 for($i = 0; $i < $len - 1; $i++) { 200 $new .= $text{$i}; 201 202 if(!is_numeric($text{$i + 1})) { 203 $new .= $spaces[array_rand($spaces)]; 204 } 205 } 206 $new .= $text{$len - 1}; 207 return $new; 208 } 209 210 /** 211 * Generate some numbers from a known string and random number 212 * 213 * @param $fixed string the fixed part, any string 214 * @param $rand float some random number between 0 and 1 215 * @return string 216 */ 217 private function _generateNumbers($fixed, $rand) { 218 $fixed = hexdec(substr(md5($fixed), 5, 5)); // use part of the md5 to generate an int 219 $rand = $rand * 0xFFFFF; // bitmask from the random number 220 return md5($rand ^ $fixed); // combine both values 221 } 222 223 /** 224 * Generates a random char string 225 * 226 * @param $fixed string the fixed part, any string 227 * @param $rand float some random number between 0 and 1 228 * @return string 229 */ 230 public function _generateCAPTCHA($fixed, $rand) { 231 $numbers = $this->_generateNumbers($fixed, $rand); 232 233 // now create the letters 234 $code = ''; 235 $lettercount = $this->getConf('lettercount') * 2; 236 if($lettercount > strlen($numbers)) $lettercount = strlen($numbers); 237 for($i = 0; $i < $lettercount; $i += 2) { 238 $code .= chr(floor(hexdec($numbers[$i].$numbers[$i + 1]) / 10) + 65); 239 } 240 241 return $code; 242 } 243 244 /** 245 * Create a mathematical task and its result 246 * 247 * @param $fixed string the fixed part, any string 248 * @param $rand float some random number between 0 and 1 249 * @return array taks, result 250 */ 251 protected function _generateMATH($fixed, $rand) { 252 $numbers = $this->_generateNumbers($fixed, $rand); 253 254 // first letter is the operator (+/-) 255 $op = (hexdec($numbers[0]) > 8) ? -1 : 1; 256 $num = array(hexdec($numbers[1].$numbers[2]), hexdec($numbers[3])); 257 258 // we only want positive results 259 if(($op < 0) && ($num[0] < $num[1])) rsort($num); 260 261 // prepare result and task text 262 $res = $num[0] + ($num[1] * $op); 263 $task = $num[0].(($op < 0) ? '-' : '+').$num[1].'=?'; 264 265 return array($task, $res); 266 } 267 268 /** 269 * Create a CAPTCHA image 270 * 271 * @param string $text the letters to display 272 */ 273 public function _imageCAPTCHA($text) { 274 $w = $this->getConf('width'); 275 $h = $this->getConf('height'); 276 277 $fonts = glob(dirname(__FILE__).'/fonts/*.ttf'); 278 279 // create a white image 280 $img = imagecreatetruecolor($w, $h); 281 $white = imagecolorallocate($img, 255, 255, 255); 282 imagefill($img, 0, 0, $white); 283 284 // add some lines as background noise 285 for($i = 0; $i < 30; $i++) { 286 $color = imagecolorallocate($img, rand(100, 250), rand(100, 250), rand(100, 250)); 287 imageline($img, rand(0, $w), rand(0, $h), rand(0, $w), rand(0, $h), $color); 288 } 289 290 // draw the letters 291 $txtlen = strlen($text); 292 for($i = 0; $i < $txtlen; $i++) { 293 $font = $fonts[array_rand($fonts)]; 294 $color = imagecolorallocate($img, rand(0, 100), rand(0, 100), rand(0, 100)); 295 $size = rand(floor($h / 1.8), floor($h * 0.7)); 296 $angle = rand(-35, 35); 297 298 $x = ($w * 0.05) + $i * floor($w * 0.9 / $txtlen); 299 $cheight = $size + ($size * 0.5); 300 $y = floor($h / 2 + $cheight / 3.8); 301 302 imagettftext($img, $size, $angle, $x, $y, $color, $font, $text[$i]); 303 } 304 305 header("Content-type: image/png"); 306 imagepng($img); 307 imagedestroy($img); 308 } 309 310 /** 311 * Encrypt the given string with the cookie salt 312 * 313 * @param string $data 314 * @return string 315 */ 316 public function encrypt($data) { 317 if(function_exists('auth_encrypt')) { 318 $data = auth_encrypt($data, auth_cookiesalt()); // since binky 319 } else { 320 $data = PMA_blowfish_encrypt($data, auth_cookiesalt()); // deprecated 321 } 322 323 return base64_encode($data); 324 } 325 326 /** 327 * Decrypt the given string with the cookie salt 328 * 329 * @param string $data 330 * @return string 331 */ 332 public function decrypt($data) { 333 $data = base64_decode($data); 334 if($data === false || $data === '') return false; 335 336 if(function_exists('auth_decrypt')) { 337 return auth_decrypt($data, auth_cookiesalt()); // since binky 338 } else { 339 return PMA_blowfish_decrypt($data, auth_cookiesalt()); // deprecated 340 } 341 } 342} 343