xref: /plugin/captcha/action.php (revision 47afabe6327ed9a98d508c677c6f0d40a3bf7c55)
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).'&amp;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).'&amp;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).'&amp;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