142a27035SAndreas Gohr<?php 242a27035SAndreas Gohr/** 342a27035SAndreas Gohr * CAPTCHA antispam plugin 442a27035SAndreas Gohr * 542a27035SAndreas Gohr * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 642a27035SAndreas Gohr * @author Andreas Gohr <gohr@cosmocode.de> 742a27035SAndreas Gohr */ 842a27035SAndreas Gohr 942a27035SAndreas Gohr// must be run within Dokuwiki 1042a27035SAndreas Gohrif(!defined('DOKU_INC')) die(); 1142a27035SAndreas Gohr 1242a27035SAndreas Gohrif(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 1342a27035SAndreas Gohrrequire_once(DOKU_PLUGIN.'action.php'); 1442a27035SAndreas Gohrrequire_once(DOKU_INC.'inc/blowfish.php'); 1542a27035SAndreas Gohr 1642a27035SAndreas Gohrclass action_plugin_captcha extends DokuWiki_Action_Plugin { 1742a27035SAndreas Gohr 1842a27035SAndreas Gohr /** 1942a27035SAndreas Gohr * return some info 2042a27035SAndreas Gohr */ 2142a27035SAndreas Gohr function getInfo(){ 2242a27035SAndreas Gohr return array( 2342a27035SAndreas Gohr 'author' => 'Andreas Gohr', 2442a27035SAndreas Gohr 'email' => 'andi@splitbrain.org', 2593f66506SAndreas Gohr 'date' => '2007-04-22', 2642a27035SAndreas Gohr 'name' => 'CAPTCHA Plugin', 2742a27035SAndreas Gohr 'desc' => 'Use a CAPTCHA challenge to protect the Wiki against automated spam', 2842a27035SAndreas Gohr 'url' => 'http://wiki:splitbrain.org/plugin:captcha', 2942a27035SAndreas Gohr ); 3042a27035SAndreas Gohr } 3142a27035SAndreas Gohr 3242a27035SAndreas Gohr /** 3342a27035SAndreas Gohr * register the eventhandlers 3442a27035SAndreas Gohr */ 3542a27035SAndreas Gohr function register(&$controller){ 3642a27035SAndreas Gohr $controller->register_hook('ACTION_ACT_PREPROCESS', 3742a27035SAndreas Gohr 'BEFORE', 3842a27035SAndreas Gohr $this, 3942a27035SAndreas Gohr 'handle_act_preprocess', 4042a27035SAndreas Gohr array()); 4142a27035SAndreas Gohr 42*47afabe6SAndreas Gohr // old hook 4342a27035SAndreas Gohr $controller->register_hook('HTML_EDITFORM_INJECTION', 4442a27035SAndreas Gohr 'BEFORE', 4542a27035SAndreas Gohr $this, 46*47afabe6SAndreas Gohr 'handle_editform_output', 47*47afabe6SAndreas Gohr array('editform' => true, 'oldhook' => true)); 48*47afabe6SAndreas Gohr 49*47afabe6SAndreas Gohr // new hook 50*47afabe6SAndreas Gohr $controller->register_hook('HTML_EDITFORM_OUTPUT', 51*47afabe6SAndreas Gohr 'BEFORE', 52*47afabe6SAndreas Gohr $this, 53*47afabe6SAndreas Gohr 'handle_editform_output', 54*47afabe6SAndreas Gohr array('editform' => true, 'oldhook' => false)); 5542a27035SAndreas Gohr 5642a27035SAndreas Gohr if($this->getConf('regprotect')){ 57*47afabe6SAndreas Gohr // old hook 5842a27035SAndreas Gohr $controller->register_hook('HTML_REGISTERFORM_INJECTION', 5942a27035SAndreas Gohr 'BEFORE', 6042a27035SAndreas Gohr $this, 61*47afabe6SAndreas Gohr 'handle_editform_output', 62*47afabe6SAndreas Gohr array('editform' => false, 'oldhook' => true)); 63*47afabe6SAndreas Gohr 64*47afabe6SAndreas Gohr // new hook 65*47afabe6SAndreas Gohr $controller->register_hook('HTML_REGISTERFORM_OUTPUT', 66*47afabe6SAndreas Gohr 'BEFORE', 67*47afabe6SAndreas Gohr $this, 68*47afabe6SAndreas Gohr 'handle_editform_output', 69*47afabe6SAndreas Gohr array('editform' => false, 'oldhook' => false)); 7042a27035SAndreas Gohr } 7142a27035SAndreas Gohr } 7242a27035SAndreas Gohr 7342a27035SAndreas Gohr /** 7442a27035SAndreas Gohr * Will intercept the 'save' action and check for CAPTCHA first. 7542a27035SAndreas Gohr */ 7642a27035SAndreas Gohr function handle_act_preprocess(&$event, $param){ 7793f66506SAndreas Gohr $act = $this->_act_clean($event->data); 7893f66506SAndreas Gohr if(!('save' == $act || ($this->getConf('regprotect') && 7993f66506SAndreas Gohr 'register' == $act && 8093f66506SAndreas Gohr $_POST['save']))){ 8193f66506SAndreas Gohr return; // nothing to do for us 8293f66506SAndreas Gohr } 8393f66506SAndreas Gohr 8442a27035SAndreas Gohr // do nothing if logged in user and no CAPTCHA required 8542a27035SAndreas Gohr if(!$this->getConf('forusers') && $_SERVER['REMOTE_USER']){ 8642a27035SAndreas Gohr return; 8742a27035SAndreas Gohr } 8842a27035SAndreas Gohr 8942a27035SAndreas Gohr // compare provided string with decrypted captcha 9042a27035SAndreas Gohr $rand = PMA_blowfish_decrypt($_REQUEST['plugin__captcha_secret'],auth_cookiesalt()); 9142a27035SAndreas Gohr $code = $this->_generateCAPTCHA($this->_fixedIdent(),$rand); 9242a27035SAndreas Gohr 9342a27035SAndreas Gohr if(!$_REQUEST['plugin__captcha_secret'] || 9442a27035SAndreas Gohr !$_REQUEST['plugin__captcha'] || 9542a27035SAndreas Gohr strtoupper($_REQUEST['plugin__captcha']) != $code){ 9693f66506SAndreas Gohr // CAPTCHA test failed! 9742a27035SAndreas Gohr msg($this->getLang('testfailed'),-1); 9893f66506SAndreas Gohr if($act == 'save'){ 9993f66506SAndreas Gohr // stay in preview mode 10042a27035SAndreas Gohr $event->data = 'preview'; 10193f66506SAndreas Gohr }else{ 10293f66506SAndreas Gohr // stay in register mode, but disable the save parameter 10393f66506SAndreas Gohr $_POST['save'] = false; 10442a27035SAndreas Gohr } 10542a27035SAndreas Gohr } 10642a27035SAndreas Gohr } 10742a27035SAndreas Gohr 10842a27035SAndreas Gohr /** 10942a27035SAndreas Gohr * Create the additional fields for the edit form 11042a27035SAndreas Gohr */ 111*47afabe6SAndreas Gohr function handle_editform_output(&$event, $param){ 112*47afabe6SAndreas Gohr // check if source view -> no captcha needed 113*47afabe6SAndreas Gohr if(!$param['oldhook']){ 114*47afabe6SAndreas Gohr // get position of submit button 115*47afabe6SAndreas Gohr $pos = $event->data->findElementByAttribute('type','submit'); 116*47afabe6SAndreas Gohr if(!$pos) return; // no button -> source view mode 117*47afabe6SAndreas Gohr }elseif($param['editform'] && !$event->data['writable']){ 11842a27035SAndreas Gohr if($param['editform'] && !$event->data['writable']) return; 119*47afabe6SAndreas Gohr } 120*47afabe6SAndreas Gohr 12142a27035SAndreas Gohr // do nothing if logged in user and no CAPTCHA required 12242a27035SAndreas Gohr if(!$this->getConf('forusers') && $_SERVER['REMOTE_USER']){ 12342a27035SAndreas Gohr return; 12442a27035SAndreas Gohr } 12542a27035SAndreas Gohr 12642a27035SAndreas Gohr global $ID; 12742a27035SAndreas Gohr 12842a27035SAndreas Gohr $rand = (float) (rand(0,10000))/10000; 12942a27035SAndreas Gohr $code = $this->_generateCAPTCHA($this->_fixedIdent(),$rand); 13042a27035SAndreas Gohr $secret = PMA_blowfish_encrypt($rand,auth_cookiesalt()); 13142a27035SAndreas Gohr 132*47afabe6SAndreas Gohr $out = ''; 133*47afabe6SAndreas Gohr $out .= '<div id="plugin__captcha_wrapper">'; 134*47afabe6SAndreas Gohr $out .= '<input type="hidden" name="plugin__captcha_secret" value="'.hsc($secret).'" />'; 135*47afabe6SAndreas Gohr $out .= '<label for="plugin__captcha">'.$this->getLang('fillcaptcha').'</label> '; 136*47afabe6SAndreas Gohr $out .= '<input type="text" size="5" maxlength="5" name="plugin__captcha" id="plugin__captcha" class="edit" /> '; 13742a27035SAndreas Gohr switch($this->getConf('mode')){ 13842a27035SAndreas Gohr case 'text': 139*47afabe6SAndreas Gohr $out .= $code; 14042a27035SAndreas Gohr break; 14142a27035SAndreas Gohr case 'js': 142*47afabe6SAndreas Gohr $out .= '<span id="plugin__captcha_code">'.$code.'</span>'; 14342a27035SAndreas Gohr break; 14442a27035SAndreas Gohr case 'image': 145*47afabe6SAndreas Gohr $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&id='.$ID.'" '. 14642a27035SAndreas Gohr ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> '; 14742a27035SAndreas Gohr break; 14842a27035SAndreas Gohr case 'audio': 149*47afabe6SAndreas Gohr $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&id='.$ID.'" '. 15042a27035SAndreas Gohr ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> '; 151*47afabe6SAndreas Gohr $out .= '<a href="'.DOKU_BASE.'lib/plugins/captcha/wav.php?secret='.rawurlencode($secret).'&id='.$ID.'"'. 15242a27035SAndreas Gohr ' class="JSnocheck" title="'.$this->getLang('soundlink').'">'; 153*47afabe6SAndreas Gohr $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/sound.png" width="16" height="16"'. 15442a27035SAndreas Gohr ' alt="'.$this->getLang('soundlink').'" /></a>'; 15542a27035SAndreas Gohr break; 15642a27035SAndreas Gohr } 157*47afabe6SAndreas Gohr $out .= '</div>'; 158*47afabe6SAndreas Gohr 159*47afabe6SAndreas Gohr if($param['oldhook']){ 160*47afabe6SAndreas Gohr // old wiki - just print 161*47afabe6SAndreas Gohr echo $out; 162*47afabe6SAndreas Gohr }else{ 163*47afabe6SAndreas Gohr // new wiki - insert at correct position 164*47afabe6SAndreas Gohr $event->data->insertElement($pos++,$out); 165*47afabe6SAndreas Gohr } 16642a27035SAndreas Gohr } 16742a27035SAndreas Gohr 16842a27035SAndreas Gohr /** 16942a27035SAndreas Gohr * Build a semi-secret fixed string identifying the current page and user 17042a27035SAndreas Gohr * 17142a27035SAndreas Gohr * This string is always the same for the current user when editing the same 17242a27035SAndreas Gohr * page revision. 17342a27035SAndreas Gohr */ 17442a27035SAndreas Gohr function _fixedIdent(){ 17542a27035SAndreas Gohr global $ID; 17642a27035SAndreas Gohr $lm = @filemtime(wikiFN($ID)); 17742a27035SAndreas Gohr return auth_browseruid(). 17842a27035SAndreas Gohr auth_cookiesalt(). 17942a27035SAndreas Gohr $ID.$lm; 18042a27035SAndreas Gohr } 18142a27035SAndreas Gohr 18242a27035SAndreas Gohr /** 18342a27035SAndreas Gohr * Pre-Sanitize the action command 18442a27035SAndreas Gohr * 18542a27035SAndreas Gohr * Similar to act_clean in action.php but simplified and without 18642a27035SAndreas Gohr * error messages 18742a27035SAndreas Gohr */ 18842a27035SAndreas Gohr function _act_clean($act){ 18942a27035SAndreas Gohr // check if the action was given as array key 19042a27035SAndreas Gohr if(is_array($act)){ 19142a27035SAndreas Gohr list($act) = array_keys($act); 19242a27035SAndreas Gohr } 19342a27035SAndreas Gohr 19442a27035SAndreas Gohr //remove all bad chars 19542a27035SAndreas Gohr $act = strtolower($act); 19642a27035SAndreas Gohr $act = preg_replace('/[^a-z_]+/','',$act); 19742a27035SAndreas Gohr 19842a27035SAndreas Gohr return $act; 19942a27035SAndreas Gohr } 20042a27035SAndreas Gohr 20142a27035SAndreas Gohr /** 20242a27035SAndreas Gohr * Generates a random 5 char string 20342a27035SAndreas Gohr * 20442a27035SAndreas Gohr * @param $fixed string - the fixed part, any string 20542a27035SAndreas Gohr * @param $rand float - some random number between 0 and 1 20642a27035SAndreas Gohr */ 20742a27035SAndreas Gohr function _generateCAPTCHA($fixed,$rand){ 20842a27035SAndreas Gohr $fixed = hexdec(substr(md5($fixed),5,5)); // use part of the md5 to generate an int 20942a27035SAndreas Gohr $rand = $rand * $fixed; // combine both values 21042a27035SAndreas Gohr 21142a27035SAndreas Gohr // seed the random generator 21242a27035SAndreas Gohr srand($rand); 21342a27035SAndreas Gohr 21442a27035SAndreas Gohr // now create the letters 21542a27035SAndreas Gohr $code = ''; 21642a27035SAndreas Gohr for($i=0;$i<5;$i++){ 21742a27035SAndreas Gohr $code .= chr(rand(65, 90)); 21842a27035SAndreas Gohr } 21942a27035SAndreas Gohr 22042a27035SAndreas Gohr // restore a really random seed 22142a27035SAndreas Gohr srand(); 22242a27035SAndreas Gohr 22342a27035SAndreas Gohr return $code; 22442a27035SAndreas Gohr } 22542a27035SAndreas Gohr 22642a27035SAndreas Gohr /** 22742a27035SAndreas Gohr * Create a CAPTCHA image 22842a27035SAndreas Gohr */ 22942a27035SAndreas Gohr function _imageCAPTCHA($text){ 23042a27035SAndreas Gohr $w = $this->getConf('width'); 23142a27035SAndreas Gohr $h = $this->getConf('height'); 23242a27035SAndreas Gohr 23342a27035SAndreas Gohr // create a white image 23442a27035SAndreas Gohr $img = imagecreate($w, $h); 23542a27035SAndreas Gohr imagecolorallocate($img, 255, 255, 255); 23642a27035SAndreas Gohr 23742a27035SAndreas Gohr // add some lines as background noise 23842a27035SAndreas Gohr for ($i = 0; $i < 30; $i++) { 23942a27035SAndreas Gohr $color = imagecolorallocate($img,rand(100, 250),rand(100, 250),rand(100, 250)); 24042a27035SAndreas Gohr imageline($img,rand(0,$w),rand(0,$h),rand(0,$w),rand(0,$h),$color); 24142a27035SAndreas Gohr } 24242a27035SAndreas Gohr 24342a27035SAndreas Gohr // draw the letters 24442a27035SAndreas Gohr for ($i = 0; $i < strlen($text); $i++){ 24542a27035SAndreas Gohr $font = dirname(__FILE__).'/VeraSe.ttf'; 24642a27035SAndreas Gohr $color = imagecolorallocate($img, rand(0, 100), rand(0, 100), rand(0, 100)); 24742a27035SAndreas Gohr $size = rand(floor($h/1.8),floor($h*0.7)); 24842a27035SAndreas Gohr $angle = rand(-35, 35); 24942a27035SAndreas Gohr 25042a27035SAndreas Gohr $x = ($w*0.05) + $i * floor($w*0.9/5); 25142a27035SAndreas Gohr $cheight = $size + ($size*0.5); 25242a27035SAndreas Gohr $y = floor($h / 2 + $cheight / 3.8); 25342a27035SAndreas Gohr 25442a27035SAndreas Gohr imagettftext($img, $size, $angle, $x, $y, $color, $font, $text[$i]); 25542a27035SAndreas Gohr } 25642a27035SAndreas Gohr 25742a27035SAndreas Gohr header("Content-type: image/png"); 25842a27035SAndreas Gohr imagepng($img); 25942a27035SAndreas Gohr imagedestroy($img); 26042a27035SAndreas Gohr } 26142a27035SAndreas Gohr} 26242a27035SAndreas Gohr 263