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