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 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 /** 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 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 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 } else { 49 $code = $this->_generateCAPTCHA($this->_fixedIdent(),$rand); 50 $text = $this->getLang('fillcaptcha'); 51 } 52 $secret = PMA_blowfish_encrypt($rand,auth_cookiesalt()); 53 54 $out = ''; 55 $out .= '<div id="plugin__captcha_wrapper">'; 56 $out .= '<input type="hidden" name="'.$this->field_sec.'" value="'.hsc($secret).'" />'; 57 $out .= '<label for="plugin__captcha">'.$text.'</label> '; 58 59 switch($this->getConf('mode')){ 60 case 'text': 61 case 'math': 62 $out .= $code; 63 break; 64 case 'js': 65 $out .= '<span id="plugin__captcha_code">'.$code.'</span>'; 66 break; 67 case 'image': 68 $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&id='.$ID.'" '. 69 ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> '; 70 break; 71 case 'audio': 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 $out .= '<a href="'.DOKU_BASE.'lib/plugins/captcha/wav.php?secret='.rawurlencode($secret).'&id='.$ID.'"'. 75 ' class="JSnocheck" title="'.$this->getLang('soundlink').'">'; 76 $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/sound.png" width="16" height="16"'. 77 ' alt="'.$this->getLang('soundlink').'" /></a>'; 78 break; 79 case 'figlet': 80 require_once(dirname(__FILE__).'/figlet.php'); 81 $figlet = new phpFiglet(); 82 if($figlet->loadfont(dirname(__FILE__).'/figlet.flf')){ 83 $out .= '<pre>'; 84 $out .= rtrim($figlet->fetch($code)); 85 $out .= '</pre>'; 86 }else{ 87 msg('Failed to load figlet.flf font file. CAPTCHA broken',-1); 88 } 89 break; 90 } 91 $out .= ' <input type="text" size="5" maxlength="5" name="'.$this->field_in.'" class="edit" /> '; 92 93 // add honeypot field 94 $out .= '<label class="no">Please keep this field empty: <input type="text" name="'.$this->field_hp.'" /></label>'; 95 $out .= '</div>'; 96 return $out; 97 } 98 99 /** 100 * Checks if the the CAPTCHA was solved correctly 101 * 102 * @param bool $msg when true, an error will be signalled through the msg() method 103 * @return bool true when the answer was correct, otherwise false 104 */ 105 function check($msg=true){ 106 // compare provided string with decrypted captcha 107 $rand = PMA_blowfish_decrypt($_REQUEST[$this->field_sec],auth_cookiesalt()); 108 109 if($this->getConf('mode') == 'math'){ 110 $code = $this->_generateMATH($this->_fixedIdent(),$rand); 111 $code = $code[1]; 112 }else{ 113 $code = $this->_generateCAPTCHA($this->_fixedIdent(),$rand); 114 } 115 116 if(!$_REQUEST[$this->field_sec] || 117 !$_REQUEST[$this->field_in] || 118 strtoupper($_REQUEST[$this->field_in]) != $code || 119 trim($_REQUEST[$this->field_hp]) !== '' 120 ){ 121 if($msg) msg($this->getLang('testfailed'),-1); 122 return false; 123 } 124 return true; 125 } 126 127 /** 128 * Build a semi-secret fixed string identifying the current page and user 129 * 130 * This string is always the same for the current user when editing the same 131 * page revision, but only for one day. Editing a page before midnight and saving 132 * after midnight will result in a failed CAPTCHA once, but makes sure it can 133 * not be reused which is especially important for the registration form where the 134 * $ID usually won't change. 135 */ 136 function _fixedIdent(){ 137 global $ID; 138 $lm = @filemtime(wikiFN($ID)); 139 $td = date('Y-m-d'); 140 return auth_browseruid(). 141 auth_cookiesalt(). 142 $ID.$lm.$td; 143 } 144 145 /** 146 * Generates a random 5 char string 147 * 148 * @param $fixed string - the fixed part, any string 149 * @param $rand float - some random number between 0 and 1 150 * @return string 151 */ 152 function _generateCAPTCHA($fixed,$rand){ 153 $fixed = hexdec(substr(md5($fixed),5,5)); // use part of the md5 to generate an int 154 $numbers = md5($rand * $fixed); // combine both values 155 156 // now create the letters 157 $code = ''; 158 for($i=0;$i<10;$i+=2){ 159 $code .= chr(floor(hexdec($numbers[$i].$numbers[$i+1])/10) + 65); 160 } 161 162 return $code; 163 } 164 165 /** 166 * Create a mathematical task and its result 167 * 168 * @param $fixed string - the fixed part, any string 169 * @param $rand float - some random number between 0 and 1 170 * @return string 171 */ 172 function _generateMATH($fixed, $rand){ 173 $fixed = hexdec(substr(md5($fixed),5,5)); // use part of the md5 to generate an int 174 $numbers = md5($rand * $fixed); // combine both values 175 176 // first letter is the operator (+/-) 177 $op = (hexdec($numbers[0]) > 8 ) ? -1 : 1; 178 $num = array(hexdec($numbers[1].$numbers[2]), hexdec($numbers[3])); 179 180 // we only want positive results 181 if(($op < 0) && ($num[0] < $num[1])) rsort($num); 182 183 // prepare result and task text 184 $res = $num[0] + ($num[1] * $op); 185 $task = $num[0] . (($op < 0) ? ' - ' : ' + ') . $num[1] . ' = ?'; 186 187 return array($task, $res); 188 } 189 190 /** 191 * Create a CAPTCHA image 192 */ 193 function _imageCAPTCHA($text){ 194 $w = $this->getConf('width'); 195 $h = $this->getConf('height'); 196 197 $fonts = glob(dirname(__FILE__).'/fonts/*.ttf'); 198 199 // create a white image 200 $img = imagecreate($w, $h); 201 imagecolorallocate($img, 255, 255, 255); 202 203 // add some lines as background noise 204 for ($i = 0; $i < 30; $i++) { 205 $color = imagecolorallocate($img,rand(100, 250),rand(100, 250),rand(100, 250)); 206 imageline($img,rand(0,$w),rand(0,$h),rand(0,$w),rand(0,$h),$color); 207 } 208 209 // draw the letters 210 for ($i = 0; $i < strlen($text); $i++){ 211 $font = $fonts[array_rand($fonts)]; 212 $color = imagecolorallocate($img, rand(0, 100), rand(0, 100), rand(0, 100)); 213 $size = rand(floor($h/1.8),floor($h*0.7)); 214 $angle = rand(-35, 35); 215 216 $x = ($w*0.05) + $i * floor($w*0.9/5); 217 $cheight = $size + ($size*0.5); 218 $y = floor($h / 2 + $cheight / 3.8); 219 220 imagettftext($img, $size, $angle, $x, $y, $color, $font, $text[$i]); 221 } 222 223 header("Content-type: image/png"); 224 imagepng($img); 225 imagedestroy($img); 226 } 227 228 229} 230