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