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