xref: /plugin/captcha/helper.php (revision 104ec26843094478ee39495271dc0fadce69ca23)
1<?php
2/**
3 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
4 * @author     Andreas Gohr <andi@splitbrain.org>
5 */
6// must be run within Dokuwiki
7if(!defined('DOKU_INC')) die();
8if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/');
9require_once(DOKU_INC.'inc/blowfish.php');
10
11class helper_plugin_captcha extends DokuWiki_Plugin {
12
13    protected $field_in = 'plugin__captcha';
14    protected $field_sec = 'plugin__captcha_secret';
15    protected $field_hp = 'plugin__captcha_honeypot';
16
17    /**
18     * Constructor. Initializes field names
19     */
20    public function __construct() {
21        $this->field_in  = md5($this->_fixedIdent().$this->field_in);
22        $this->field_sec = md5($this->_fixedIdent().$this->field_sec);
23        $this->field_hp  = md5($this->_fixedIdent().$this->field_hp);
24    }
25
26    /**
27     * Check if the CAPTCHA should be used. Always check this before using the methods below.
28     *
29     * @return bool true when the CAPTCHA should be used
30     */
31    public function isEnabled() {
32        if(!$this->getConf('forusers') && $_SERVER['REMOTE_USER']) return false;
33        return true;
34    }
35
36    /**
37     * Returns the HTML to display the CAPTCHA with the chosen method
38     */
39    public function getHTML() {
40        global $ID;
41
42        $rand = (float) (rand(0, 10000)) / 10000;
43        if($this->getConf('mode') == 'math') {
44            $code = $this->_generateMATH($this->_fixedIdent(), $rand);
45            $code = $code[0];
46            $text = $this->getLang('fillmath');
47        } else {
48            $code = $this->_generateCAPTCHA($this->_fixedIdent(), $rand);
49            $text = $this->getLang('fillcaptcha');
50        }
51        $secret = PMA_blowfish_encrypt($rand, auth_cookiesalt());
52
53        $out = '';
54        $out .= '<div id="plugin__captcha_wrapper">';
55        $out .= '<input type="hidden" name="'.$this->field_sec.'" value="'.hsc($secret).'" />';
56        $out .= '<label for="plugin__captcha">'.$text.'</label> ';
57
58        switch($this->getConf('mode')) {
59            case 'text':
60            case 'math':
61                $out .= $code;
62                break;
63            case 'js':
64                $out .= '<span id="plugin__captcha_code">'.$code.'</span>';
65                break;
66            case 'image':
67                $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&amp;id='.$ID.'" '.
68                    ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> ';
69                break;
70            case 'audio':
71                $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&amp;id='.$ID.'" '.
72                    ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> ';
73                $out .= '<a href="'.DOKU_BASE.'lib/plugins/captcha/wav.php?secret='.rawurlencode($secret).'&amp;id='.$ID.'"'.
74                    ' class="JSnocheck" title="'.$this->getLang('soundlink').'">';
75                $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/sound.png" width="16" height="16"'.
76                    ' alt="'.$this->getLang('soundlink').'" /></a>';
77                break;
78            case 'figlet':
79                require_once(dirname(__FILE__).'/figlet.php');
80                $figlet = new phpFiglet();
81                if($figlet->loadfont(dirname(__FILE__).'/figlet.flf')) {
82                    $out .= '<pre>';
83                    $out .= rtrim($figlet->fetch($code));
84                    $out .= '</pre>';
85                } else {
86                    msg('Failed to load figlet.flf font file. CAPTCHA broken', -1);
87                }
88                break;
89        }
90        $out .= ' <input type="text" size="5" maxlength="5" name="'.$this->field_in.'" class="edit" /> ';
91
92        // add honeypot field
93        $out .= '<label class="no">Please keep this field empty: <input type="text" name="'.$this->field_hp.'" /></label>';
94        $out .= '</div>';
95        return $out;
96    }
97
98    /**
99     * Checks if the the CAPTCHA was solved correctly
100     *
101     * @param  bool $msg when true, an error will be signalled through the msg() method
102     * @return bool true when the answer was correct, otherwise false
103     */
104    public function check($msg = true) {
105        // compare provided string with decrypted captcha
106        $rand = PMA_blowfish_decrypt($_REQUEST[$this->field_sec], auth_cookiesalt());
107
108        if($this->getConf('mode') == 'math') {
109            $code = $this->_generateMATH($this->_fixedIdent(), $rand);
110            $code = $code[1];
111        } else {
112            $code = $this->_generateCAPTCHA($this->_fixedIdent(), $rand);
113        }
114
115        if(!$_REQUEST[$this->field_sec] ||
116            !$_REQUEST[$this->field_in] ||
117            strtoupper($_REQUEST[$this->field_in]) != $code ||
118            trim($_REQUEST[$this->field_hp]) !== ''
119        ) {
120            if($msg) msg($this->getLang('testfailed'), -1);
121            return false;
122        }
123        return true;
124    }
125
126    /**
127     * Build a semi-secret fixed string identifying the current page and user
128     *
129     * This string is always the same for the current user when editing the same
130     * page revision, but only for one day. Editing a page before midnight and saving
131     * after midnight will result in a failed CAPTCHA once, but makes sure it can
132     * not be reused which is especially important for the registration form where the
133     * $ID usually won't change.
134     *
135     * @return string
136     */
137    public function _fixedIdent() {
138        global $ID;
139        $lm = @filemtime(wikiFN($ID));
140        $td = date('Y-m-d');
141        return auth_browseruid().
142            auth_cookiesalt().
143            $ID.$lm.$td;
144    }
145
146    /**
147     * Generates a random 5 char string
148     *
149     * @param $fixed string the fixed part, any string
150     * @param $rand  float  some random number between 0 and 1
151     * @return string
152     */
153    public function _generateCAPTCHA($fixed, $rand) {
154        $fixed   = hexdec(substr(md5($fixed), 5, 5)); // use part of the md5 to generate an int
155        $numbers = md5($rand * $fixed); // combine both values
156
157        // now create the letters
158        $code = '';
159        for($i = 0; $i < 10; $i += 2) {
160            $code .= chr(floor(hexdec($numbers[$i].$numbers[$i + 1]) / 10) + 65);
161        }
162
163        return $code;
164    }
165
166    /**
167     * Create a mathematical task and its result
168     *
169     * @param $fixed string the fixed part, any string
170     * @param $rand  float  some random number between 0 and 1
171     * @return array taks, result
172     */
173    protected function _generateMATH($fixed, $rand) {
174        $fixed   = hexdec(substr(md5($fixed), 5, 5)); // use part of the md5 to generate an int
175        $numbers = md5($rand * $fixed); // combine both values
176
177        // first letter is the operator (+/-)
178        $op  = (hexdec($numbers[0]) > 8) ? -1 : 1;
179        $num = array(hexdec($numbers[1].$numbers[2]), hexdec($numbers[3]));
180
181        // we only want positive results
182        if(($op < 0) && ($num[0] < $num[1])) rsort($num);
183
184        // prepare result and task text
185        $res  = $num[0] + ($num[1] * $op);
186        $task = $num[0].(($op < 0) ? '&nbsp;-&nbsp;' : '&nbsp;+&nbsp;').$num[1].'&nbsp;=&nbsp;?';
187
188        return array($task, $res);
189    }
190
191    /**
192     * Create a CAPTCHA image
193     *
194     * @param string $text the letters to display
195     */
196    public function _imageCAPTCHA($text) {
197        $w = $this->getConf('width');
198        $h = $this->getConf('height');
199
200        $fonts = glob(dirname(__FILE__).'/fonts/*.ttf');
201
202        // create a white image
203        $img = imagecreate($w, $h);
204        imagecolorallocate($img, 255, 255, 255);
205
206        // add some lines as background noise
207        for($i = 0; $i < 30; $i++) {
208            $color = imagecolorallocate($img, rand(100, 250), rand(100, 250), rand(100, 250));
209            imageline($img, rand(0, $w), rand(0, $h), rand(0, $w), rand(0, $h), $color);
210        }
211
212        // draw the letters
213        for($i = 0; $i < strlen($text); $i++) {
214            $font  = $fonts[array_rand($fonts)];
215            $color = imagecolorallocate($img, rand(0, 100), rand(0, 100), rand(0, 100));
216            $size  = rand(floor($h / 1.8), floor($h * 0.7));
217            $angle = rand(-35, 35);
218
219            $x       = ($w * 0.05) + $i * floor($w * 0.9 / 5);
220            $cheight = $size + ($size * 0.5);
221            $y       = floor($h / 2 + $cheight / 3.8);
222
223            imagettftext($img, $size, $angle, $x, $y, $color, $font, $text[$i]);
224        }
225
226        header("Content-type: image/png");
227        imagepng($img);
228        imagedestroy($img);
229    }
230
231}
232