1<?php 2/** 3 * CAPTCHA antispam plugin 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <gohr@cosmocode.de> 7 */ 8 9// must be run within Dokuwiki 10if(!defined('DOKU_INC')) die(); 11 12if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 13require_once(DOKU_PLUGIN.'action.php'); 14require_once(DOKU_INC.'inc/blowfish.php'); 15 16class action_plugin_captcha extends DokuWiki_Action_Plugin { 17 18 /** 19 * return some info 20 */ 21 function getInfo(){ 22 return array( 23 'author' => 'Andreas Gohr', 24 'email' => 'andi@splitbrain.org', 25 'date' => '2006-12-02', 26 'name' => 'CAPTCHA Plugin', 27 'desc' => 'Use a CAPTCHA challenge to protect the Wiki against automated spam', 28 'url' => 'http://wiki:splitbrain.org/plugin:captcha', 29 ); 30 } 31 32 /** 33 * register the eventhandlers 34 */ 35 function register(&$controller){ 36 $controller->register_hook('ACTION_ACT_PREPROCESS', 37 'BEFORE', 38 $this, 39 'handle_act_preprocess', 40 array()); 41 42 $controller->register_hook('HTML_EDITFORM_INJECTION', 43 'BEFORE', 44 $this, 45 'handle_editform_injection', 46 array('editform' => true)); 47 48 if($this->getConf('regprotect')){ 49 $controller->register_hook('ACTION_REGISTER', 50 'BEFORE', 51 $this, 52 'handle_act_register', 53 array()); 54 55 $controller->register_hook('HTML_REGISTERFORM_INJECTION', 56 'BEFORE', 57 $this, 58 'handle_editform_injection', 59 array('editform' => false)); 60 } 61 } 62 63 /** 64 * Will intercept the 'save' action and check for CAPTCHA first. 65 */ 66 function handle_act_preprocess(&$event, $param){ 67 if('save' != $this->_act_clean($event->data)) return; // nothing to do for us 68 // do nothing if logged in user and no CAPTCHA required 69 if(!$this->getConf('forusers') && $_SERVER['REMOTE_USER']){ 70 return; 71 } 72 73 74 // compare provided string with decrypted captcha 75 $rand = PMA_blowfish_decrypt($_REQUEST['plugin__captcha_secret'],auth_cookiesalt()); 76 $code = $this->_generateCAPTCHA($this->_fixedIdent(),$rand); 77 78 if(!$_REQUEST['plugin__captcha_secret'] || 79 !$_REQUEST['plugin__captcha'] || 80 strtoupper($_REQUEST['plugin__captcha']) != $code){ 81 // CAPTCHA test failed! Continue to edit instead of saving 82 msg($this->getLang('testfailed'),-1); 83 $event->data = 'preview'; 84 } 85 // if we arrive here it was a valid save 86 } 87 88 /** 89 * Will intercept the register process and check for CAPTCHA first. 90 */ 91 function handle_act_register(&$event, $param){ 92 // compare provided string with decrypted captcha 93 $rand = PMA_blowfish_decrypt($_REQUEST['plugin__captcha_secret'],auth_cookiesalt()); 94 $code = $this->_generateCAPTCHA($this->_fixedIdent(),$rand); 95 96 if(!$_REQUEST['plugin__captcha_secret'] || 97 !$_REQUEST['plugin__captcha'] || 98 strtoupper($_REQUEST['plugin__captcha']) != $code){ 99 // CAPTCHA test failed! Continue to edit instead of saving 100 msg($this->getLang('testfailed'),-1); 101 $event->preventDefault(); 102 $event->stopPropagation(); 103 return false; 104 } 105 // if we arrive here it was a valid save 106 } 107 108 109 /** 110 * Create the additional fields for the edit form 111 */ 112 function handle_editform_injection(&$event, $param){ 113 if($param['editform'] && !$event->data['writable']) return; 114 // do nothing if logged in user and no CAPTCHA required 115 if(!$this->getConf('forusers') && $_SERVER['REMOTE_USER']){ 116 return; 117 } 118 119 global $ID; 120 121 $rand = (float) (rand(0,10000))/10000; 122 $code = $this->_generateCAPTCHA($this->_fixedIdent(),$rand); 123 $secret = PMA_blowfish_encrypt($rand,auth_cookiesalt()); 124 125 echo '<div id="plugin__captcha_wrapper">'; 126 echo '<input type="hidden" name="plugin__captcha_secret" value="'.hsc($secret).'" />'; 127 echo '<label for="plugin__captcha">'.$this->getLang('fillcaptcha').'</label> '; 128 echo '<input type="text" size="5" maxlength="5" name="plugin__captcha" id="plugin__captcha" class="edit" /> '; 129 switch($this->getConf('mode')){ 130 case 'text': 131 echo $code; 132 break; 133 case 'js': 134 echo '<span id="plugin__captcha_code">'.$code.'</span>'; 135 break; 136 case 'image': 137 echo '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&id='.$ID.'" '. 138 ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> '; 139 break; 140 case 'audio': 141 echo '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&id='.$ID.'" '. 142 ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> '; 143 echo '<a href="'.DOKU_BASE.'lib/plugins/captcha/wav.php?secret='.rawurlencode($secret).'&id='.$ID.'"'. 144 ' class="JSnocheck" title="'.$this->getLang('soundlink').'">'; 145 echo '<img src="'.DOKU_BASE.'lib/plugins/captcha/sound.png" width="16" height="16"'. 146 ' alt="'.$this->getLang('soundlink').'" /></a>'; 147 break; 148 } 149 echo '</div>'; 150 } 151 152 /** 153 * Build a semi-secret fixed string identifying the current page and user 154 * 155 * This string is always the same for the current user when editing the same 156 * page revision. 157 */ 158 function _fixedIdent(){ 159 global $ID; 160 $lm = @filemtime(wikiFN($ID)); 161 return auth_browseruid(). 162 auth_cookiesalt(). 163 $ID.$lm; 164 } 165 166 /** 167 * Pre-Sanitize the action command 168 * 169 * Similar to act_clean in action.php but simplified and without 170 * error messages 171 */ 172 function _act_clean($act){ 173 // check if the action was given as array key 174 if(is_array($act)){ 175 list($act) = array_keys($act); 176 } 177 178 //remove all bad chars 179 $act = strtolower($act); 180 $act = preg_replace('/[^a-z_]+/','',$act); 181 182 return $act; 183 } 184 185 /** 186 * Generates a random 5 char string 187 * 188 * @param $fixed string - the fixed part, any string 189 * @param $rand float - some random number between 0 and 1 190 */ 191 function _generateCAPTCHA($fixed,$rand){ 192 $fixed = hexdec(substr(md5($fixed),5,5)); // use part of the md5 to generate an int 193 $rand = $rand * $fixed; // combine both values 194 195 // seed the random generator 196 srand($rand); 197 198 // now create the letters 199 $code = ''; 200 for($i=0;$i<5;$i++){ 201 $code .= chr(rand(65, 90)); 202 } 203 204 // restore a really random seed 205 srand(); 206 207 return $code; 208 } 209 210 /** 211 * Create a CAPTCHA image 212 */ 213 function _imageCAPTCHA($text){ 214 $w = $this->getConf('width'); 215 $h = $this->getConf('height'); 216 217 // create a white image 218 $img = imagecreate($w, $h); 219 imagecolorallocate($img, 255, 255, 255); 220 221 // add some lines as background noise 222 for ($i = 0; $i < 30; $i++) { 223 $color = imagecolorallocate($img,rand(100, 250),rand(100, 250),rand(100, 250)); 224 imageline($img,rand(0,$w),rand(0,$h),rand(0,$w),rand(0,$h),$color); 225 } 226 227 // draw the letters 228 for ($i = 0; $i < strlen($text); $i++){ 229 $font = dirname(__FILE__).'/VeraSe.ttf'; 230 $color = imagecolorallocate($img, rand(0, 100), rand(0, 100), rand(0, 100)); 231 $size = rand(floor($h/1.8),floor($h*0.7)); 232 $angle = rand(-35, 35); 233 234 $x = ($w*0.05) + $i * floor($w*0.9/5); 235 $cheight = $size + ($size*0.5); 236 $y = floor($h / 2 + $cheight / 3.8); 237 238 imagettftext($img, $size, $angle, $x, $y, $color, $font, $text[$i]); 239 } 240 241 header("Content-type: image/png"); 242 imagepng($img); 243 imagedestroy($img); 244 } 245} 246 247