xref: /plugin/captcha/helper.php (revision 9d6f09af71c3493cfadb47eecf0e4073f05982bb)
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 */
677e00bf9SAndreas Gohr// must be run within Dokuwiki
777e00bf9SAndreas Gohrif(!defined('DOKU_INC')) die();
877e00bf9SAndreas Gohrif(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/');
977e00bf9SAndreas Gohrrequire_once(DOKU_INC.'inc/blowfish.php');
1077e00bf9SAndreas Gohr
1177e00bf9SAndreas Gohrclass helper_plugin_captcha extends DokuWiki_Plugin {
1277e00bf9SAndreas Gohr
1323d379a8SAndreas Gohr    protected $field_in = 'plugin__captcha';
1423d379a8SAndreas Gohr    protected $field_sec = 'plugin__captcha_secret';
1523d379a8SAndreas Gohr    protected $field_hp = 'plugin__captcha_honeypot';
1623d379a8SAndreas Gohr
1723d379a8SAndreas Gohr    /**
1823d379a8SAndreas Gohr     * Constructor. Initializes field names
1923d379a8SAndreas Gohr     */
20104ec268SAndreas Gohr    public function __construct() {
2123d379a8SAndreas Gohr        $this->field_in  = md5($this->_fixedIdent().$this->field_in);
2223d379a8SAndreas Gohr        $this->field_sec = md5($this->_fixedIdent().$this->field_sec);
2323d379a8SAndreas Gohr        $this->field_hp  = md5($this->_fixedIdent().$this->field_hp);
2423d379a8SAndreas Gohr    }
2523d379a8SAndreas Gohr
2677e00bf9SAndreas Gohr    /**
2777e00bf9SAndreas Gohr     * Check if the CAPTCHA should be used. Always check this before using the methods below.
2877e00bf9SAndreas Gohr     *
2977e00bf9SAndreas Gohr     * @return bool true when the CAPTCHA should be used
3077e00bf9SAndreas Gohr     */
31104ec268SAndreas Gohr    public function isEnabled() {
3277e00bf9SAndreas Gohr        if(!$this->getConf('forusers') && $_SERVER['REMOTE_USER']) return false;
3377e00bf9SAndreas Gohr        return true;
3477e00bf9SAndreas Gohr    }
3577e00bf9SAndreas Gohr
3677e00bf9SAndreas Gohr    /**
3777e00bf9SAndreas Gohr     * Returns the HTML to display the CAPTCHA with the chosen method
3877e00bf9SAndreas Gohr     */
39104ec268SAndreas Gohr    public function getHTML() {
4077e00bf9SAndreas Gohr        global $ID;
4177e00bf9SAndreas Gohr
4277e00bf9SAndreas Gohr        $rand = (float) (rand(0, 10000)) / 10000;
439e312724SAndreas Gohr        if($this->getConf('mode') == 'math') {
449e312724SAndreas Gohr            $code = $this->_generateMATH($this->_fixedIdent(), $rand);
459e312724SAndreas Gohr            $code = $code[0];
469e312724SAndreas Gohr            $text = $this->getLang('fillmath');
479e312724SAndreas Gohr        } else {
4877e00bf9SAndreas Gohr            $code = $this->_generateCAPTCHA($this->_fixedIdent(), $rand);
499e312724SAndreas Gohr            $text = $this->getLang('fillcaptcha');
509e312724SAndreas Gohr        }
5177e00bf9SAndreas Gohr        $secret = PMA_blowfish_encrypt($rand, auth_cookiesalt());
5277e00bf9SAndreas Gohr
5328c14643SAndreas Gohr        $txtlen = $this->getConf('lettercount');
5428c14643SAndreas Gohr
5577e00bf9SAndreas Gohr        $out = '';
5677e00bf9SAndreas Gohr        $out .= '<div id="plugin__captcha_wrapper">';
5723d379a8SAndreas Gohr        $out .= '<input type="hidden" name="'.$this->field_sec.'" value="'.hsc($secret).'" />';
589e312724SAndreas Gohr        $out .= '<label for="plugin__captcha">'.$text.'</label> ';
5923d379a8SAndreas Gohr
6077e00bf9SAndreas Gohr        switch($this->getConf('mode')) {
619e312724SAndreas Gohr            case 'math':
62*9d6f09afSAndreas Gohr            case 'text':
63*9d6f09afSAndreas Gohr                $out .= $this->_obfuscateText($code);
6477e00bf9SAndreas Gohr                break;
6577e00bf9SAndreas Gohr            case 'js':
66*9d6f09afSAndreas Gohr                $out .= '<span id="plugin__captcha_code">'.$this->_obfuscateText($code).'</span>';
6777e00bf9SAndreas Gohr                break;
6877e00bf9SAndreas Gohr            case 'image':
6977e00bf9SAndreas Gohr                $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&amp;id='.$ID.'" '.
7077e00bf9SAndreas Gohr                    ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> ';
7177e00bf9SAndreas Gohr                break;
7277e00bf9SAndreas Gohr            case 'audio':
7377e00bf9SAndreas Gohr                $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&amp;id='.$ID.'" '.
7477e00bf9SAndreas Gohr                    ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> ';
7577e00bf9SAndreas Gohr                $out .= '<a href="'.DOKU_BASE.'lib/plugins/captcha/wav.php?secret='.rawurlencode($secret).'&amp;id='.$ID.'"'.
7677e00bf9SAndreas Gohr                    ' class="JSnocheck" title="'.$this->getLang('soundlink').'">';
7777e00bf9SAndreas Gohr                $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/sound.png" width="16" height="16"'.
7877e00bf9SAndreas Gohr                    ' alt="'.$this->getLang('soundlink').'" /></a>';
7977e00bf9SAndreas Gohr                break;
8052e95008SAndreas Gohr            case 'figlet':
8152e95008SAndreas Gohr                require_once(dirname(__FILE__).'/figlet.php');
8252e95008SAndreas Gohr                $figlet = new phpFiglet();
8352e95008SAndreas Gohr                if($figlet->loadfont(dirname(__FILE__).'/figlet.flf')) {
8452e95008SAndreas Gohr                    $out .= '<pre>';
8552e95008SAndreas Gohr                    $out .= rtrim($figlet->fetch($code));
8652e95008SAndreas Gohr                    $out .= '</pre>';
8752e95008SAndreas Gohr                } else {
8852e95008SAndreas Gohr                    msg('Failed to load figlet.flf font file. CAPTCHA broken', -1);
8952e95008SAndreas Gohr                }
9052e95008SAndreas Gohr                break;
9177e00bf9SAndreas Gohr        }
9228c14643SAndreas Gohr        $out .= ' <input type="text" size="'.$txtlen.'" maxlength="'.$txtlen.'" name="'.$this->field_in.'" class="edit" /> ';
9323d379a8SAndreas Gohr
9423d379a8SAndreas Gohr        // add honeypot field
9523d379a8SAndreas Gohr        $out .= '<label class="no">Please keep this field empty: <input type="text" name="'.$this->field_hp.'" /></label>';
9677e00bf9SAndreas Gohr        $out .= '</div>';
9777e00bf9SAndreas Gohr        return $out;
9877e00bf9SAndreas Gohr    }
9977e00bf9SAndreas Gohr
10077e00bf9SAndreas Gohr    /**
10177e00bf9SAndreas Gohr     * Checks if the the CAPTCHA was solved correctly
10277e00bf9SAndreas Gohr     *
10377e00bf9SAndreas Gohr     * @param  bool $msg when true, an error will be signalled through the msg() method
10477e00bf9SAndreas Gohr     * @return bool true when the answer was correct, otherwise false
10577e00bf9SAndreas Gohr     */
106104ec268SAndreas Gohr    public function check($msg = true) {
10777e00bf9SAndreas Gohr        // compare provided string with decrypted captcha
10823d379a8SAndreas Gohr        $rand = PMA_blowfish_decrypt($_REQUEST[$this->field_sec], auth_cookiesalt());
1099e312724SAndreas Gohr
1109e312724SAndreas Gohr        if($this->getConf('mode') == 'math') {
1119e312724SAndreas Gohr            $code = $this->_generateMATH($this->_fixedIdent(), $rand);
1129e312724SAndreas Gohr            $code = $code[1];
1139e312724SAndreas Gohr        } else {
11477e00bf9SAndreas Gohr            $code = $this->_generateCAPTCHA($this->_fixedIdent(), $rand);
1159e312724SAndreas Gohr        }
11677e00bf9SAndreas Gohr
11723d379a8SAndreas Gohr        if(!$_REQUEST[$this->field_sec] ||
11823d379a8SAndreas Gohr            !$_REQUEST[$this->field_in] ||
11923d379a8SAndreas Gohr            strtoupper($_REQUEST[$this->field_in]) != $code ||
12023d379a8SAndreas Gohr            trim($_REQUEST[$this->field_hp]) !== ''
12123d379a8SAndreas Gohr        ) {
12277e00bf9SAndreas Gohr            if($msg) msg($this->getLang('testfailed'), -1);
12377e00bf9SAndreas Gohr            return false;
12477e00bf9SAndreas Gohr        }
12577e00bf9SAndreas Gohr        return true;
12677e00bf9SAndreas Gohr    }
12777e00bf9SAndreas Gohr
12877e00bf9SAndreas Gohr    /**
12977e00bf9SAndreas Gohr     * Build a semi-secret fixed string identifying the current page and user
13077e00bf9SAndreas Gohr     *
13177e00bf9SAndreas Gohr     * This string is always the same for the current user when editing the same
13227d84d8dSAndreas Gohr     * page revision, but only for one day. Editing a page before midnight and saving
13327d84d8dSAndreas Gohr     * after midnight will result in a failed CAPTCHA once, but makes sure it can
13427d84d8dSAndreas Gohr     * not be reused which is especially important for the registration form where the
13527d84d8dSAndreas Gohr     * $ID usually won't change.
136104ec268SAndreas Gohr     *
137104ec268SAndreas Gohr     * @return string
13877e00bf9SAndreas Gohr     */
139104ec268SAndreas Gohr    public function _fixedIdent() {
14077e00bf9SAndreas Gohr        global $ID;
14177e00bf9SAndreas Gohr        $lm = @filemtime(wikiFN($ID));
14227d84d8dSAndreas Gohr        $td = date('Y-m-d');
14377e00bf9SAndreas Gohr        return auth_browseruid().
14477e00bf9SAndreas Gohr            auth_cookiesalt().
14527d84d8dSAndreas Gohr            $ID.$lm.$td;
14677e00bf9SAndreas Gohr    }
14777e00bf9SAndreas Gohr
14877e00bf9SAndreas Gohr    /**
149*9d6f09afSAndreas Gohr     * Adds random space characters within the given text
150*9d6f09afSAndreas Gohr     *
151*9d6f09afSAndreas Gohr     * Keeps subsequent numbers without spaces (for math problem)
152*9d6f09afSAndreas Gohr     *
153*9d6f09afSAndreas Gohr     * @param $text
154*9d6f09afSAndreas Gohr     * @return string
155*9d6f09afSAndreas Gohr     */
156*9d6f09afSAndreas Gohr    protected function _obfuscateText($text){
157*9d6f09afSAndreas Gohr        $new = '';
158*9d6f09afSAndreas Gohr
159*9d6f09afSAndreas Gohr        $spaces = array(
160*9d6f09afSAndreas Gohr            "\r",
161*9d6f09afSAndreas Gohr            "\n",
162*9d6f09afSAndreas Gohr            "\r\n",
163*9d6f09afSAndreas Gohr            ' ',
164*9d6f09afSAndreas Gohr            "\xC2\xA0", // \u00A0    NO-BREAK SPACE
165*9d6f09afSAndreas Gohr            "\xE2\x80\x80", // \u2000    EN QUAD
166*9d6f09afSAndreas Gohr            "\xE2\x80\x81", // \u2001    EM QUAD
167*9d6f09afSAndreas Gohr            "\xE2\x80\x82", // \u2002    EN SPACE
168*9d6f09afSAndreas Gohr   //         "\xE2\x80\x83", // \u2003    EM SPACE
169*9d6f09afSAndreas Gohr            "\xE2\x80\x84", // \u2004    THREE-PER-EM SPACE
170*9d6f09afSAndreas Gohr            "\xE2\x80\x85", // \u2005    FOUR-PER-EM SPACE
171*9d6f09afSAndreas Gohr            "\xE2\x80\x86", // \u2006    SIX-PER-EM SPACE
172*9d6f09afSAndreas Gohr            "\xE2\x80\x87", // \u2007    FIGURE SPACE
173*9d6f09afSAndreas Gohr            "\xE2\x80\x88", // \u2008    PUNCTUATION SPACE
174*9d6f09afSAndreas Gohr            "\xE2\x80\x89", // \u2009    THIN SPACE
175*9d6f09afSAndreas Gohr            "\xE2\x80\x8A", // \u200A    HAIR SPACE
176*9d6f09afSAndreas Gohr            "\xE2\x80\xAF", // \u202F    NARROW NO-BREAK SPACE
177*9d6f09afSAndreas Gohr            "\xE2\x81\x9F", // \u205F    MEDIUM MATHEMATICAL SPACE
178*9d6f09afSAndreas Gohr
179*9d6f09afSAndreas Gohr            "\xE1\xA0\x8E\r\n", // \u180E    MONGOLIAN VOWEL SEPARATOR
180*9d6f09afSAndreas Gohr            "\xE2\x80\x8B\r\n", // \u200B    ZERO WIDTH SPACE
181*9d6f09afSAndreas Gohr            "\xEF\xBB\xBF\r\n", // \uFEFF    ZERO WIDTH NO-BREAK SPACE
182*9d6f09afSAndreas Gohr        );
183*9d6f09afSAndreas Gohr
184*9d6f09afSAndreas Gohr        $len = strlen($text);
185*9d6f09afSAndreas Gohr        for($i=0; $i<$len-1; $i++){
186*9d6f09afSAndreas Gohr            $new .= $text{$i};
187*9d6f09afSAndreas Gohr
188*9d6f09afSAndreas Gohr            if(!is_numeric($text{$i+1})){
189*9d6f09afSAndreas Gohr                $new .= $spaces[array_rand($spaces)];
190*9d6f09afSAndreas Gohr            }
191*9d6f09afSAndreas Gohr        }
192*9d6f09afSAndreas Gohr        $new .= $text{$len-1};
193*9d6f09afSAndreas Gohr        return $new;
194*9d6f09afSAndreas Gohr    }
195*9d6f09afSAndreas Gohr
196*9d6f09afSAndreas Gohr    /**
19728c14643SAndreas Gohr     * Generates a random char string
19877e00bf9SAndreas Gohr     *
199104ec268SAndreas Gohr     * @param $fixed string the fixed part, any string
200104ec268SAndreas Gohr     * @param $rand  float  some random number between 0 and 1
20123d379a8SAndreas Gohr     * @return string
20277e00bf9SAndreas Gohr     */
203104ec268SAndreas Gohr    public function _generateCAPTCHA($fixed, $rand) {
20477e00bf9SAndreas Gohr        $fixed   = hexdec(substr(md5($fixed), 5, 5)); // use part of the md5 to generate an int
20577e00bf9SAndreas Gohr        $numbers = md5($rand * $fixed); // combine both values
20677e00bf9SAndreas Gohr
20777e00bf9SAndreas Gohr        // now create the letters
20877e00bf9SAndreas Gohr        $code = '';
20928c14643SAndreas Gohr        for($i = 0; $i < ($this->getConf('lettercount') * 2); $i += 2) {
21077e00bf9SAndreas Gohr            $code .= chr(floor(hexdec($numbers[$i].$numbers[$i + 1]) / 10) + 65);
21177e00bf9SAndreas Gohr        }
21277e00bf9SAndreas Gohr
21377e00bf9SAndreas Gohr        return $code;
21477e00bf9SAndreas Gohr    }
21577e00bf9SAndreas Gohr
2169e312724SAndreas Gohr    /**
2179e312724SAndreas Gohr     * Create a mathematical task and its result
2189e312724SAndreas Gohr     *
219104ec268SAndreas Gohr     * @param $fixed string the fixed part, any string
220104ec268SAndreas Gohr     * @param $rand  float  some random number between 0 and 1
221104ec268SAndreas Gohr     * @return array taks, result
2229e312724SAndreas Gohr     */
223104ec268SAndreas Gohr    protected function _generateMATH($fixed, $rand) {
2249e312724SAndreas Gohr        $fixed   = hexdec(substr(md5($fixed), 5, 5)); // use part of the md5 to generate an int
2259e312724SAndreas Gohr        $numbers = md5($rand * $fixed); // combine both values
2269e312724SAndreas Gohr
2279e312724SAndreas Gohr        // first letter is the operator (+/-)
2289e312724SAndreas Gohr        $op  = (hexdec($numbers[0]) > 8) ? -1 : 1;
2299e312724SAndreas Gohr        $num = array(hexdec($numbers[1].$numbers[2]), hexdec($numbers[3]));
2309e312724SAndreas Gohr
2319e312724SAndreas Gohr        // we only want positive results
2329e312724SAndreas Gohr        if(($op < 0) && ($num[0] < $num[1])) rsort($num);
2339e312724SAndreas Gohr
2349e312724SAndreas Gohr        // prepare result and task text
2359e312724SAndreas Gohr        $res  = $num[0] + ($num[1] * $op);
236*9d6f09afSAndreas Gohr        $task = $num[0].(($op < 0) ? '-' : '+').$num[1].'=?';
2379e312724SAndreas Gohr
2389e312724SAndreas Gohr        return array($task, $res);
2399e312724SAndreas Gohr    }
24077e00bf9SAndreas Gohr
24177e00bf9SAndreas Gohr    /**
24277e00bf9SAndreas Gohr     * Create a CAPTCHA image
243104ec268SAndreas Gohr     *
244104ec268SAndreas Gohr     * @param string $text the letters to display
24577e00bf9SAndreas Gohr     */
246104ec268SAndreas Gohr    public function _imageCAPTCHA($text) {
24777e00bf9SAndreas Gohr        $w = $this->getConf('width');
24877e00bf9SAndreas Gohr        $h = $this->getConf('height');
24977e00bf9SAndreas Gohr
250dc091fd0SAndreas Gohr        $fonts = glob(dirname(__FILE__).'/fonts/*.ttf');
251dc091fd0SAndreas Gohr
25277e00bf9SAndreas Gohr        // create a white image
25328c14643SAndreas Gohr        $img = imagecreatetruecolor($w, $h);
25428c14643SAndreas Gohr        $white = imagecolorallocate($img, 255, 255, 255);
25528c14643SAndreas Gohr        imagefill($img, 0, 0, $white);
25677e00bf9SAndreas Gohr
25777e00bf9SAndreas Gohr        // add some lines as background noise
25877e00bf9SAndreas Gohr        for($i = 0; $i < 30; $i++) {
25977e00bf9SAndreas Gohr            $color = imagecolorallocate($img, rand(100, 250), rand(100, 250), rand(100, 250));
26077e00bf9SAndreas Gohr            imageline($img, rand(0, $w), rand(0, $h), rand(0, $w), rand(0, $h), $color);
26177e00bf9SAndreas Gohr        }
26277e00bf9SAndreas Gohr
26377e00bf9SAndreas Gohr        // draw the letters
26428c14643SAndreas Gohr        $txtlen = strlen($text);
26528c14643SAndreas Gohr        for($i = 0; $i < $txtlen; $i++) {
266dc091fd0SAndreas Gohr            $font  = $fonts[array_rand($fonts)];
26777e00bf9SAndreas Gohr            $color = imagecolorallocate($img, rand(0, 100), rand(0, 100), rand(0, 100));
26877e00bf9SAndreas Gohr            $size  = rand(floor($h / 1.8), floor($h * 0.7));
26977e00bf9SAndreas Gohr            $angle = rand(-35, 35);
27077e00bf9SAndreas Gohr
27128c14643SAndreas Gohr            $x       = ($w * 0.05) + $i * floor($w * 0.9 / $txtlen);
27277e00bf9SAndreas Gohr            $cheight = $size + ($size * 0.5);
27377e00bf9SAndreas Gohr            $y       = floor($h / 2 + $cheight / 3.8);
27477e00bf9SAndreas Gohr
27577e00bf9SAndreas Gohr            imagettftext($img, $size, $angle, $x, $y, $color, $font, $text[$i]);
27677e00bf9SAndreas Gohr        }
27777e00bf9SAndreas Gohr
27877e00bf9SAndreas Gohr        header("Content-type: image/png");
27977e00bf9SAndreas Gohr        imagepng($img);
28077e00bf9SAndreas Gohr        imagedestroy($img);
28177e00bf9SAndreas Gohr    }
28277e00bf9SAndreas Gohr
28377e00bf9SAndreas Gohr}
284