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