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