xref: /plugin/captcha/helper.php (revision 09870f9957f41d4b115b3b65d985a710498d7596)
177e00bf9SAndreas Gohr<?php
277e00bf9SAndreas Gohr/**
377e00bf9SAndreas Gohr * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
477e00bf9SAndreas Gohr * @author     Andreas Gohr <andi@splitbrain.org>
577e00bf9SAndreas Gohr */
67218f96cSAndreas Gohr
777e00bf9SAndreas Gohr// must be run within Dokuwiki
877e00bf9SAndreas Gohrif(!defined('DOKU_INC')) die();
977e00bf9SAndreas Gohrif(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/');
107218f96cSAndreas Gohr
1177e00bf9SAndreas Gohr
1277e00bf9SAndreas Gohrclass helper_plugin_captcha extends DokuWiki_Plugin {
1377e00bf9SAndreas Gohr
1423d379a8SAndreas Gohr    protected $field_in = 'plugin__captcha';
1523d379a8SAndreas Gohr    protected $field_sec = 'plugin__captcha_secret';
1623d379a8SAndreas Gohr    protected $field_hp = 'plugin__captcha_honeypot';
1723d379a8SAndreas Gohr
1823d379a8SAndreas Gohr    /**
1923d379a8SAndreas Gohr     * Constructor. Initializes field names
2023d379a8SAndreas Gohr     */
21104ec268SAndreas Gohr    public function __construct() {
2223d379a8SAndreas Gohr        $this->field_in  = md5($this->_fixedIdent().$this->field_in);
2323d379a8SAndreas Gohr        $this->field_sec = md5($this->_fixedIdent().$this->field_sec);
2423d379a8SAndreas Gohr        $this->field_hp  = md5($this->_fixedIdent().$this->field_hp);
2523d379a8SAndreas Gohr    }
2623d379a8SAndreas Gohr
2777e00bf9SAndreas Gohr    /**
2877e00bf9SAndreas Gohr     * Check if the CAPTCHA should be used. Always check this before using the methods below.
2977e00bf9SAndreas Gohr     *
3077e00bf9SAndreas Gohr     * @return bool true when the CAPTCHA should be used
3177e00bf9SAndreas Gohr     */
32104ec268SAndreas Gohr    public function isEnabled() {
3377e00bf9SAndreas Gohr        if(!$this->getConf('forusers') && $_SERVER['REMOTE_USER']) return false;
3477e00bf9SAndreas Gohr        return true;
3577e00bf9SAndreas Gohr    }
3677e00bf9SAndreas Gohr
3777e00bf9SAndreas Gohr    /**
3877e00bf9SAndreas Gohr     * Returns the HTML to display the CAPTCHA with the chosen method
3977e00bf9SAndreas Gohr     */
40104ec268SAndreas Gohr    public function getHTML() {
4177e00bf9SAndreas Gohr        global $ID;
4277e00bf9SAndreas Gohr
4377e00bf9SAndreas Gohr        $rand = (float) (rand(0, 10000)) / 10000;
449e312724SAndreas Gohr        if($this->getConf('mode') == 'math') {
459e312724SAndreas Gohr            $code = $this->_generateMATH($this->_fixedIdent(), $rand);
469e312724SAndreas Gohr            $code = $code[0];
479e312724SAndreas Gohr            $text = $this->getLang('fillmath');
48df8afac4SAndreas Gohr        } elseif($this->getConf('mode') == 'question') {
49df8afac4SAndreas Gohr            $text = $this->getConf('question');
509e312724SAndreas Gohr        } else {
5177e00bf9SAndreas Gohr            $code = $this->_generateCAPTCHA($this->_fixedIdent(), $rand);
529e312724SAndreas Gohr            $text = $this->getLang('fillcaptcha');
539e312724SAndreas Gohr        }
54f044313dSAndreas Gohr        $secret = $this->encrypt($rand);
5577e00bf9SAndreas Gohr
5628c14643SAndreas Gohr        $txtlen = $this->getConf('lettercount');
5728c14643SAndreas Gohr
5877e00bf9SAndreas Gohr        $out = '';
5977e00bf9SAndreas Gohr        $out .= '<div id="plugin__captcha_wrapper">';
6023d379a8SAndreas Gohr        $out .= '<input type="hidden" name="'.$this->field_sec.'" value="'.hsc($secret).'" />';
619e312724SAndreas Gohr        $out .= '<label for="plugin__captcha">'.$text.'</label> ';
6223d379a8SAndreas Gohr
6377e00bf9SAndreas Gohr        switch($this->getConf('mode')) {
649e312724SAndreas Gohr            case 'math':
659d6f09afSAndreas Gohr            case 'text':
669d6f09afSAndreas Gohr                $out .= $this->_obfuscateText($code);
6777e00bf9SAndreas Gohr                break;
6877e00bf9SAndreas Gohr            case 'js':
699d6f09afSAndreas Gohr                $out .= '<span id="plugin__captcha_code">'.$this->_obfuscateText($code).'</span>';
7077e00bf9SAndreas Gohr                break;
7177e00bf9SAndreas Gohr            case 'image':
7277e00bf9SAndreas Gohr                $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&amp;id='.$ID.'" '.
7377e00bf9SAndreas Gohr                    ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> ';
7477e00bf9SAndreas Gohr                break;
7577e00bf9SAndreas Gohr            case 'audio':
7677e00bf9SAndreas Gohr                $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&amp;id='.$ID.'" '.
7777e00bf9SAndreas Gohr                    ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> ';
7877e00bf9SAndreas Gohr                $out .= '<a href="'.DOKU_BASE.'lib/plugins/captcha/wav.php?secret='.rawurlencode($secret).'&amp;id='.$ID.'"'.
7977e00bf9SAndreas Gohr                    ' class="JSnocheck" title="'.$this->getLang('soundlink').'">';
8077e00bf9SAndreas Gohr                $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/sound.png" width="16" height="16"'.
8177e00bf9SAndreas Gohr                    ' alt="'.$this->getLang('soundlink').'" /></a>';
8277e00bf9SAndreas Gohr                break;
8352e95008SAndreas Gohr            case 'figlet':
8452e95008SAndreas Gohr                require_once(dirname(__FILE__).'/figlet.php');
8552e95008SAndreas Gohr                $figlet = new phpFiglet();
8652e95008SAndreas Gohr                if($figlet->loadfont(dirname(__FILE__).'/figlet.flf')) {
8752e95008SAndreas Gohr                    $out .= '<pre>';
8852e95008SAndreas Gohr                    $out .= rtrim($figlet->fetch($code));
8952e95008SAndreas Gohr                    $out .= '</pre>';
9052e95008SAndreas Gohr                } else {
9152e95008SAndreas Gohr                    msg('Failed to load figlet.flf font file. CAPTCHA broken', -1);
9252e95008SAndreas Gohr                }
9352e95008SAndreas Gohr                break;
9477e00bf9SAndreas Gohr        }
95df8afac4SAndreas Gohr        $out .= ' <input type="text" size="'.$txtlen.'" name="'.$this->field_in.'" class="edit" /> ';
9623d379a8SAndreas Gohr
9723d379a8SAndreas Gohr        // add honeypot field
989d63c05fSlainme        $out .= '<label class="no">'.$this->getLang('honeypot').'<input type="text" name="'.$this->field_hp.'" /></label>';
9977e00bf9SAndreas Gohr        $out .= '</div>';
10077e00bf9SAndreas Gohr        return $out;
10177e00bf9SAndreas Gohr    }
10277e00bf9SAndreas Gohr
10377e00bf9SAndreas Gohr    /**
10477e00bf9SAndreas Gohr     * Checks if the the CAPTCHA was solved correctly
10577e00bf9SAndreas Gohr     *
10677e00bf9SAndreas Gohr     * @param  bool $msg when true, an error will be signalled through the msg() method
10777e00bf9SAndreas Gohr     * @return bool true when the answer was correct, otherwise false
10877e00bf9SAndreas Gohr     */
109104ec268SAndreas Gohr    public function check($msg = true) {
110478e363cSAndreas Gohr        global $INPUT;
111478e363cSAndreas Gohr
112478e363cSAndreas Gohr        $code = '';
113478e363cSAndreas Gohr        $field_sec = $INPUT->str($this->field_sec);
114478e363cSAndreas Gohr        $field_in  = $INPUT->str($this->field_in);
115478e363cSAndreas Gohr        $field_hp  = $INPUT->str($this->field_hp);
116478e363cSAndreas Gohr
117478e363cSAndreas Gohr        // reconstruct captcha from provided $field_sec
118478e363cSAndreas Gohr        if($field_sec) {
119478e363cSAndreas Gohr            $rand = $this->decrypt($field_sec);
1209e312724SAndreas Gohr
1219e312724SAndreas Gohr            if($this->getConf('mode') == 'math') {
1229e312724SAndreas Gohr                $code = $this->_generateMATH($this->_fixedIdent(), $rand);
1239e312724SAndreas Gohr                $code = $code[1];
124df8afac4SAndreas Gohr            } elseif($this->getConf('mode') == 'question') {
125df8afac4SAndreas Gohr                $code = $this->getConf('answer');
1269e312724SAndreas Gohr            } else {
12777e00bf9SAndreas Gohr                $code = $this->_generateCAPTCHA($this->_fixedIdent(), $rand);
1289e312724SAndreas Gohr            }
129478e363cSAndreas Gohr        }
13077e00bf9SAndreas Gohr
131478e363cSAndreas Gohr        // compare values
132478e363cSAndreas Gohr        if(!$field_sec ||
133478e363cSAndreas Gohr            !$field_in ||
134478e363cSAndreas Gohr            utf8_strtolower($field_in) != utf8_strtolower($code) ||
135478e363cSAndreas Gohr            trim($field_hp) !== ''
13623d379a8SAndreas Gohr        ) {
13777e00bf9SAndreas Gohr            if($msg) msg($this->getLang('testfailed'), -1);
13877e00bf9SAndreas Gohr            return false;
13977e00bf9SAndreas Gohr        }
14077e00bf9SAndreas Gohr        return true;
14177e00bf9SAndreas Gohr    }
14277e00bf9SAndreas Gohr
14377e00bf9SAndreas Gohr    /**
14477e00bf9SAndreas Gohr     * Build a semi-secret fixed string identifying the current page and user
14577e00bf9SAndreas Gohr     *
14677e00bf9SAndreas Gohr     * This string is always the same for the current user when editing the same
14727d84d8dSAndreas Gohr     * page revision, but only for one day. Editing a page before midnight and saving
14827d84d8dSAndreas Gohr     * after midnight will result in a failed CAPTCHA once, but makes sure it can
14927d84d8dSAndreas Gohr     * not be reused which is especially important for the registration form where the
15027d84d8dSAndreas Gohr     * $ID usually won't change.
151104ec268SAndreas Gohr     *
152104ec268SAndreas Gohr     * @return string
15377e00bf9SAndreas Gohr     */
154104ec268SAndreas Gohr    public function _fixedIdent() {
15577e00bf9SAndreas Gohr        global $ID;
15677e00bf9SAndreas Gohr        $lm = @filemtime(wikiFN($ID));
15727d84d8dSAndreas Gohr        $td = date('Y-m-d');
15877e00bf9SAndreas Gohr        return auth_browseruid().
15977e00bf9SAndreas Gohr        auth_cookiesalt().
16027d84d8dSAndreas Gohr        $ID.$lm.$td;
16177e00bf9SAndreas Gohr    }
16277e00bf9SAndreas Gohr
16377e00bf9SAndreas Gohr    /**
1649d6f09afSAndreas Gohr     * Adds random space characters within the given text
1659d6f09afSAndreas Gohr     *
1669d6f09afSAndreas Gohr     * Keeps subsequent numbers without spaces (for math problem)
1679d6f09afSAndreas Gohr     *
1689d6f09afSAndreas Gohr     * @param $text
1699d6f09afSAndreas Gohr     * @return string
1709d6f09afSAndreas Gohr     */
1719d6f09afSAndreas Gohr    protected function _obfuscateText($text) {
1729d6f09afSAndreas Gohr        $new = '';
1739d6f09afSAndreas Gohr
1749d6f09afSAndreas Gohr        $spaces = array(
1759d6f09afSAndreas Gohr            "\r",
1769d6f09afSAndreas Gohr            "\n",
1779d6f09afSAndreas Gohr            "\r\n",
1789d6f09afSAndreas Gohr            ' ',
1799d6f09afSAndreas Gohr            "\xC2\xA0", // \u00A0    NO-BREAK SPACE
1809d6f09afSAndreas Gohr            "\xE2\x80\x80", // \u2000    EN QUAD
1819d6f09afSAndreas Gohr            "\xE2\x80\x81", // \u2001    EM QUAD
1829d6f09afSAndreas Gohr            "\xE2\x80\x82", // \u2002    EN SPACE
1839d6f09afSAndreas Gohr            //         "\xE2\x80\x83", // \u2003    EM SPACE
1849d6f09afSAndreas Gohr            "\xE2\x80\x84", // \u2004    THREE-PER-EM SPACE
1859d6f09afSAndreas Gohr            "\xE2\x80\x85", // \u2005    FOUR-PER-EM SPACE
1869d6f09afSAndreas Gohr            "\xE2\x80\x86", // \u2006    SIX-PER-EM SPACE
1879d6f09afSAndreas Gohr            "\xE2\x80\x87", // \u2007    FIGURE SPACE
1889d6f09afSAndreas Gohr            "\xE2\x80\x88", // \u2008    PUNCTUATION SPACE
1899d6f09afSAndreas Gohr            "\xE2\x80\x89", // \u2009    THIN SPACE
1909d6f09afSAndreas Gohr            "\xE2\x80\x8A", // \u200A    HAIR SPACE
1919d6f09afSAndreas Gohr            "\xE2\x80\xAF", // \u202F    NARROW NO-BREAK SPACE
1929d6f09afSAndreas Gohr            "\xE2\x81\x9F", // \u205F    MEDIUM MATHEMATICAL SPACE
1939d6f09afSAndreas Gohr
1949d6f09afSAndreas Gohr            "\xE1\xA0\x8E\r\n", // \u180E    MONGOLIAN VOWEL SEPARATOR
1959d6f09afSAndreas Gohr            "\xE2\x80\x8B\r\n", // \u200B    ZERO WIDTH SPACE
1969d6f09afSAndreas Gohr            "\xEF\xBB\xBF\r\n", // \uFEFF    ZERO WIDTH NO-BREAK SPACE
1979d6f09afSAndreas Gohr        );
1989d6f09afSAndreas Gohr
1999d6f09afSAndreas Gohr        $len = strlen($text);
2009d6f09afSAndreas Gohr        for($i = 0; $i < $len - 1; $i++) {
2019d6f09afSAndreas Gohr            $new .= $text{$i};
2029d6f09afSAndreas Gohr
2039d6f09afSAndreas Gohr            if(!is_numeric($text{$i + 1})) {
2049d6f09afSAndreas Gohr                $new .= $spaces[array_rand($spaces)];
2059d6f09afSAndreas Gohr            }
2069d6f09afSAndreas Gohr        }
2079d6f09afSAndreas Gohr        $new .= $text{$len - 1};
2089d6f09afSAndreas Gohr        return $new;
2099d6f09afSAndreas Gohr    }
2109d6f09afSAndreas Gohr
2119d6f09afSAndreas Gohr    /**
212a02b2219SPatrick Brown     * Generate some numbers from a known string and random number
213a02b2219SPatrick Brown     *
214a02b2219SPatrick Brown     * @param $fixed string the fixed part, any string
215a02b2219SPatrick Brown     * @param $rand  float  some random number between 0 and 1
216a02b2219SPatrick Brown     * @return string
217a02b2219SPatrick Brown     */
218a02b2219SPatrick Brown    private function _generateNumbers($fixed, $rand) {
219a02b2219SPatrick Brown        $fixed   = hexdec(substr(md5($fixed), 5, 5)); // use part of the md5 to generate an int
220a02b2219SPatrick Brown        $rand = $rand * 0xFFFFF; // bitmask from the random number
221a02b2219SPatrick Brown        return md5($rand ^ $fixed); // combine both values
222a02b2219SPatrick Brown    }
223a02b2219SPatrick Brown
224a02b2219SPatrick Brown    /**
22528c14643SAndreas Gohr     * Generates a random char string
22677e00bf9SAndreas Gohr     *
227104ec268SAndreas Gohr     * @param $fixed string the fixed part, any string
228104ec268SAndreas Gohr     * @param $rand  float  some random number between 0 and 1
22923d379a8SAndreas Gohr     * @return string
23077e00bf9SAndreas Gohr     */
231104ec268SAndreas Gohr    public function _generateCAPTCHA($fixed, $rand) {
232a02b2219SPatrick Brown        $numbers = $this->_generateNumbers($fixed, $rand);
23377e00bf9SAndreas Gohr
23477e00bf9SAndreas Gohr        // now create the letters
23577e00bf9SAndreas Gohr        $code = '';
2369a516edaSPatrick Brown        $lettercount = $this->getConf('lettercount') * 2;
2379a516edaSPatrick Brown        if($lettercount > strlen($numbers)) $lettercount = strlen($numbers);
2389a516edaSPatrick Brown        for($i = 0; $i < $lettercount; $i += 2) {
23977e00bf9SAndreas Gohr            $code .= chr(floor(hexdec($numbers[$i].$numbers[$i + 1]) / 10) + 65);
24077e00bf9SAndreas Gohr        }
24177e00bf9SAndreas Gohr
24277e00bf9SAndreas Gohr        return $code;
24377e00bf9SAndreas Gohr    }
24477e00bf9SAndreas Gohr
2459e312724SAndreas Gohr    /**
2469e312724SAndreas Gohr     * Create a mathematical task and its result
2479e312724SAndreas Gohr     *
248104ec268SAndreas Gohr     * @param $fixed string the fixed part, any string
249104ec268SAndreas Gohr     * @param $rand  float  some random number between 0 and 1
250104ec268SAndreas Gohr     * @return array taks, result
2519e312724SAndreas Gohr     */
252104ec268SAndreas Gohr    protected function _generateMATH($fixed, $rand) {
253a02b2219SPatrick Brown        $numbers = $this->_generateNumbers($fixed, $rand);
2549e312724SAndreas Gohr
2559e312724SAndreas Gohr        // first letter is the operator (+/-)
2569e312724SAndreas Gohr        $op  = (hexdec($numbers[0]) > 8) ? -1 : 1;
2579e312724SAndreas Gohr        $num = array(hexdec($numbers[1].$numbers[2]), hexdec($numbers[3]));
2589e312724SAndreas Gohr
2599e312724SAndreas Gohr        // we only want positive results
2609e312724SAndreas Gohr        if(($op < 0) && ($num[0] < $num[1])) rsort($num);
2619e312724SAndreas Gohr
2629e312724SAndreas Gohr        // prepare result and task text
2639e312724SAndreas Gohr        $res  = $num[0] + ($num[1] * $op);
2649d6f09afSAndreas Gohr        $task = $num[0].(($op < 0) ? '-' : '+').$num[1].'=?';
2659e312724SAndreas Gohr
2669e312724SAndreas Gohr        return array($task, $res);
2679e312724SAndreas Gohr    }
26877e00bf9SAndreas Gohr
26977e00bf9SAndreas Gohr    /**
27077e00bf9SAndreas Gohr     * Create a CAPTCHA image
271104ec268SAndreas Gohr     *
272104ec268SAndreas Gohr     * @param string $text the letters to display
27377e00bf9SAndreas Gohr     */
274104ec268SAndreas Gohr    public function _imageCAPTCHA($text) {
27577e00bf9SAndreas Gohr        $w = $this->getConf('width');
27677e00bf9SAndreas Gohr        $h = $this->getConf('height');
27777e00bf9SAndreas Gohr
278dc091fd0SAndreas Gohr        $fonts = glob(dirname(__FILE__).'/fonts/*.ttf');
279dc091fd0SAndreas Gohr
28077e00bf9SAndreas Gohr        // create a white image
28128c14643SAndreas Gohr        $img   = imagecreatetruecolor($w, $h);
28228c14643SAndreas Gohr        $white = imagecolorallocate($img, 255, 255, 255);
28328c14643SAndreas Gohr        imagefill($img, 0, 0, $white);
28477e00bf9SAndreas Gohr
28577e00bf9SAndreas Gohr        // add some lines as background noise
28677e00bf9SAndreas Gohr        for($i = 0; $i < 30; $i++) {
28777e00bf9SAndreas Gohr            $color = imagecolorallocate($img, rand(100, 250), rand(100, 250), rand(100, 250));
28877e00bf9SAndreas Gohr            imageline($img, rand(0, $w), rand(0, $h), rand(0, $w), rand(0, $h), $color);
28977e00bf9SAndreas Gohr        }
29077e00bf9SAndreas Gohr
29177e00bf9SAndreas Gohr        // draw the letters
29228c14643SAndreas Gohr        $txtlen = strlen($text);
29328c14643SAndreas Gohr        for($i = 0; $i < $txtlen; $i++) {
294dc091fd0SAndreas Gohr            $font  = $fonts[array_rand($fonts)];
29577e00bf9SAndreas Gohr            $color = imagecolorallocate($img, rand(0, 100), rand(0, 100), rand(0, 100));
29677e00bf9SAndreas Gohr            $size  = rand(floor($h / 1.8), floor($h * 0.7));
29777e00bf9SAndreas Gohr            $angle = rand(-35, 35);
29877e00bf9SAndreas Gohr
29928c14643SAndreas Gohr            $x       = ($w * 0.05) + $i * floor($w * 0.9 / $txtlen);
30077e00bf9SAndreas Gohr            $cheight = $size + ($size * 0.5);
30177e00bf9SAndreas Gohr            $y       = floor($h / 2 + $cheight / 3.8);
30277e00bf9SAndreas Gohr
30377e00bf9SAndreas Gohr            imagettftext($img, $size, $angle, $x, $y, $color, $font, $text[$i]);
30477e00bf9SAndreas Gohr        }
30577e00bf9SAndreas Gohr
30677e00bf9SAndreas Gohr        header("Content-type: image/png");
30777e00bf9SAndreas Gohr        imagepng($img);
30877e00bf9SAndreas Gohr        imagedestroy($img);
30977e00bf9SAndreas Gohr    }
31077e00bf9SAndreas Gohr
311f044313dSAndreas Gohr    /**
312f044313dSAndreas Gohr     * Encrypt the given string with the cookie salt
313f044313dSAndreas Gohr     *
314f044313dSAndreas Gohr     * @param string $data
315f044313dSAndreas Gohr     * @return string
316f044313dSAndreas Gohr     */
317f044313dSAndreas Gohr    public function encrypt($data) {
318f044313dSAndreas Gohr        if(function_exists('auth_encrypt')) {
319f044313dSAndreas Gohr            $data = auth_encrypt($data, auth_cookiesalt()); // since binky
320f044313dSAndreas Gohr        } else {
321f044313dSAndreas Gohr            $data = PMA_blowfish_encrypt($data, auth_cookiesalt()); // deprecated
322f044313dSAndreas Gohr        }
323f044313dSAndreas Gohr
324f044313dSAndreas Gohr        return base64_encode($data);
325f044313dSAndreas Gohr    }
326f044313dSAndreas Gohr
327f044313dSAndreas Gohr    /**
328f044313dSAndreas Gohr     * Decrypt the given string with the cookie salt
329f044313dSAndreas Gohr     *
330f044313dSAndreas Gohr     * @param string $data
331f044313dSAndreas Gohr     * @return string
332f044313dSAndreas Gohr     */
333f044313dSAndreas Gohr    public function decrypt($data) {
334f044313dSAndreas Gohr        $data = base64_decode($data);
335*09870f99SPatrick Brown        if($data === false || $data === '') return false;
336f044313dSAndreas Gohr
337f044313dSAndreas Gohr        if(function_exists('auth_decrypt')) {
338f044313dSAndreas Gohr            return auth_decrypt($data, auth_cookiesalt()); // since binky
339f044313dSAndreas Gohr        } else {
340f044313dSAndreas Gohr            return PMA_blowfish_decrypt($data, auth_cookiesalt()); // deprecated
341f044313dSAndreas Gohr        }
342f044313dSAndreas Gohr    }
34377e00bf9SAndreas Gohr}
344