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