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' => '2007-08-06', 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 // old hook 43 $controller->register_hook('HTML_EDITFORM_INJECTION', 44 'BEFORE', 45 $this, 46 'handle_editform_output', 47 array('editform' => true, 'oldhook' => true)); 48 49 // new hook 50 $controller->register_hook('HTML_EDITFORM_OUTPUT', 51 'BEFORE', 52 $this, 53 'handle_editform_output', 54 array('editform' => true, 'oldhook' => false)); 55 56 if($this->getConf('regprotect')){ 57 // old hook 58 $controller->register_hook('HTML_REGISTERFORM_INJECTION', 59 'BEFORE', 60 $this, 61 'handle_editform_output', 62 array('editform' => false, 'oldhook' => true)); 63 64 // new hook 65 $controller->register_hook('HTML_REGISTERFORM_OUTPUT', 66 'BEFORE', 67 $this, 68 'handle_editform_output', 69 array('editform' => false, 'oldhook' => false)); 70 } 71 } 72 73 /** 74 * Will intercept the 'save' action and check for CAPTCHA first. 75 */ 76 function handle_act_preprocess(&$event, $param){ 77 $act = $this->_act_clean($event->data); 78 if(!('save' == $act || ($this->getConf('regprotect') && 79 'register' == $act && 80 $_POST['save']))){ 81 return; // nothing to do for us 82 } 83 84 // do nothing if logged in user and no CAPTCHA required 85 if(!$this->getConf('forusers') && $_SERVER['REMOTE_USER']){ 86 return; 87 } 88 89 // compare provided string with decrypted captcha 90 $rand = PMA_blowfish_decrypt($_REQUEST['plugin__captcha_secret'],auth_cookiesalt()); 91 $code = $this->_generateCAPTCHA($this->_fixedIdent(),$rand); 92 93 if(!$_REQUEST['plugin__captcha_secret'] || 94 !$_REQUEST['plugin__captcha'] || 95 strtoupper($_REQUEST['plugin__captcha']) != $code){ 96 // CAPTCHA test failed! 97 msg($this->getLang('testfailed'),-1); 98 if($act == 'save'){ 99 // stay in preview mode 100 $event->data = 'preview'; 101 }else{ 102 // stay in register mode, but disable the save parameter 103 $_POST['save'] = false; 104 } 105 } 106 } 107 108 /** 109 * Create the additional fields for the edit form 110 */ 111 function handle_editform_output(&$event, $param){ 112 // check if source view -> no captcha needed 113 if(!$param['oldhook']){ 114 // get position of submit button 115 $pos = $event->data->findElementByAttribute('type','submit'); 116 if(!$pos) return; // no button -> source view mode 117 }elseif($param['editform'] && !$event->data['writable']){ 118 if($param['editform'] && !$event->data['writable']) return; 119 } 120 121 // do nothing if logged in user and no CAPTCHA required 122 if(!$this->getConf('forusers') && $_SERVER['REMOTE_USER']){ 123 return; 124 } 125 126 global $ID; 127 128 $rand = (float) (rand(0,10000))/10000; 129 $code = $this->_generateCAPTCHA($this->_fixedIdent(),$rand); 130 $secret = PMA_blowfish_encrypt($rand,auth_cookiesalt()); 131 132 $out = ''; 133 $out .= '<div id="plugin__captcha_wrapper">'; 134 $out .= '<input type="hidden" name="plugin__captcha_secret" value="'.hsc($secret).'" />'; 135 $out .= '<label for="plugin__captcha">'.$this->getLang('fillcaptcha').'</label> '; 136 $out .= '<input type="text" size="5" maxlength="5" name="plugin__captcha" id="plugin__captcha" class="edit" /> '; 137 switch($this->getConf('mode')){ 138 case 'text': 139 $out .= $code; 140 break; 141 case 'js': 142 $out .= '<span id="plugin__captcha_code">'.$code.'</span>'; 143 break; 144 case 'image': 145 $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&id='.$ID.'" '. 146 ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> '; 147 break; 148 case 'audio': 149 $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&id='.$ID.'" '. 150 ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> '; 151 $out .= '<a href="'.DOKU_BASE.'lib/plugins/captcha/wav.php?secret='.rawurlencode($secret).'&id='.$ID.'"'. 152 ' class="JSnocheck" title="'.$this->getLang('soundlink').'">'; 153 $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/sound.png" width="16" height="16"'. 154 ' alt="'.$this->getLang('soundlink').'" /></a>'; 155 break; 156 } 157 $out .= '</div>'; 158 159 if($param['oldhook']){ 160 // old wiki - just print 161 echo $out; 162 }else{ 163 // new wiki - insert at correct position 164 $event->data->insertElement($pos++,$out); 165 } 166 } 167 168 /** 169 * Build a semi-secret fixed string identifying the current page and user 170 * 171 * This string is always the same for the current user when editing the same 172 * page revision. 173 */ 174 function _fixedIdent(){ 175 global $ID; 176 $lm = @filemtime(wikiFN($ID)); 177 return auth_browseruid(). 178 auth_cookiesalt(). 179 $ID.$lm; 180 } 181 182 /** 183 * Pre-Sanitize the action command 184 * 185 * Similar to act_clean in action.php but simplified and without 186 * error messages 187 */ 188 function _act_clean($act){ 189 // check if the action was given as array key 190 if(is_array($act)){ 191 list($act) = array_keys($act); 192 } 193 194 //remove all bad chars 195 $act = strtolower($act); 196 $act = preg_replace('/[^a-z_]+/','',$act); 197 198 return $act; 199 } 200 201 /** 202 * Generates a random 5 char string 203 * 204 * @param $fixed string - the fixed part, any string 205 * @param $rand float - some random number between 0 and 1 206 */ 207 function _generateCAPTCHA($fixed,$rand){ 208 $fixed = hexdec(substr(md5($fixed),5,5)); // use part of the md5 to generate an int 209 $rand = $rand * $fixed; // combine both values 210 211 // seed the random generator 212 srand($rand); 213 214 // now create the letters 215 $code = ''; 216 for($i=0;$i<5;$i++){ 217 $code .= chr(rand(65, 90)); 218 } 219 220 // restore a really random seed 221 srand(); 222 223 return $code; 224 } 225 226 /** 227 * Create a CAPTCHA image 228 */ 229 function _imageCAPTCHA($text){ 230 $w = $this->getConf('width'); 231 $h = $this->getConf('height'); 232 233 // create a white image 234 $img = imagecreate($w, $h); 235 imagecolorallocate($img, 255, 255, 255); 236 237 // add some lines as background noise 238 for ($i = 0; $i < 30; $i++) { 239 $color = imagecolorallocate($img,rand(100, 250),rand(100, 250),rand(100, 250)); 240 imageline($img,rand(0,$w),rand(0,$h),rand(0,$w),rand(0,$h),$color); 241 } 242 243 // draw the letters 244 for ($i = 0; $i < strlen($text); $i++){ 245 $font = dirname(__FILE__).'/VeraSe.ttf'; 246 $color = imagecolorallocate($img, rand(0, 100), rand(0, 100), rand(0, 100)); 247 $size = rand(floor($h/1.8),floor($h*0.7)); 248 $angle = rand(-35, 35); 249 250 $x = ($w*0.05) + $i * floor($w*0.9/5); 251 $cheight = $size + ($size*0.5); 252 $y = floor($h / 2 + $cheight / 3.8); 253 254 imagettftext($img, $size, $angle, $x, $y, $color, $font, $text[$i]); 255 } 256 257 header("Content-type: image/png"); 258 imagepng($img); 259 imagedestroy($img); 260 } 261} 262 263