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