xref: /plugin/captcha/helper.php (revision 27d84d8d7509a09d943ca6b9e0580ac661225ed8)
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     */
2023d379a8SAndreas Gohr    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
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     */
3277e00bf9SAndreas Gohr    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     */
4077e00bf9SAndreas Gohr    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');
489e312724SAndreas Gohr        } else {
4977e00bf9SAndreas Gohr            $code = $this->_generateCAPTCHA($this->_fixedIdent(),$rand);
509e312724SAndreas Gohr            $text = $this->getLang('fillcaptcha');
519e312724SAndreas Gohr        }
5277e00bf9SAndreas Gohr        $secret = PMA_blowfish_encrypt($rand,auth_cookiesalt());
5377e00bf9SAndreas Gohr
5477e00bf9SAndreas Gohr        $out  = '';
5577e00bf9SAndreas Gohr        $out .= '<div id="plugin__captcha_wrapper">';
5623d379a8SAndreas Gohr        $out .= '<input type="hidden" name="'.$this->field_sec.'" value="'.hsc($secret).'" />';
579e312724SAndreas Gohr        $out .= '<label for="plugin__captcha">'.$text.'</label> ';
5823d379a8SAndreas Gohr
5977e00bf9SAndreas Gohr        switch($this->getConf('mode')){
6077e00bf9SAndreas Gohr            case 'text':
619e312724SAndreas Gohr            case 'math':
6277e00bf9SAndreas Gohr                $out .= $code;
6377e00bf9SAndreas Gohr                break;
6477e00bf9SAndreas Gohr            case 'js':
6577e00bf9SAndreas Gohr                $out .= '<span id="plugin__captcha_code">'.$code.'</span>';
6677e00bf9SAndreas Gohr                break;
6777e00bf9SAndreas Gohr            case 'image':
6877e00bf9SAndreas Gohr                $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&amp;id='.$ID.'" '.
6977e00bf9SAndreas Gohr                        ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> ';
7077e00bf9SAndreas Gohr                break;
7177e00bf9SAndreas Gohr            case 'audio':
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                $out .= '<a href="'.DOKU_BASE.'lib/plugins/captcha/wav.php?secret='.rawurlencode($secret).'&amp;id='.$ID.'"'.
7577e00bf9SAndreas Gohr                        ' class="JSnocheck" title="'.$this->getLang('soundlink').'">';
7677e00bf9SAndreas Gohr                $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/sound.png" width="16" height="16"'.
7777e00bf9SAndreas Gohr                        ' alt="'.$this->getLang('soundlink').'" /></a>';
7877e00bf9SAndreas Gohr                break;
7952e95008SAndreas Gohr            case 'figlet':
8052e95008SAndreas Gohr                require_once(dirname(__FILE__).'/figlet.php');
8152e95008SAndreas Gohr                $figlet = new phpFiglet();
8252e95008SAndreas Gohr                if($figlet->loadfont(dirname(__FILE__).'/figlet.flf')){
8352e95008SAndreas Gohr                    $out .= '<pre>';
8452e95008SAndreas Gohr                    $out .= rtrim($figlet->fetch($code));
8552e95008SAndreas Gohr                    $out .= '</pre>';
8652e95008SAndreas Gohr                }else{
8752e95008SAndreas Gohr                    msg('Failed to load figlet.flf font file. CAPTCHA broken',-1);
8852e95008SAndreas Gohr                }
8952e95008SAndreas Gohr                break;
9077e00bf9SAndreas Gohr        }
919e312724SAndreas Gohr        $out .= ' <input type="text" size="5" maxlength="5" name="'.$this->field_in.'" class="edit" /> ';
9223d379a8SAndreas Gohr
9323d379a8SAndreas Gohr        // add honeypot field
9423d379a8SAndreas Gohr        $out .= '<label class="no">Please keep this field empty: <input type="text" name="'.$this->field_hp.'" /></label>';
9577e00bf9SAndreas Gohr        $out .= '</div>';
9677e00bf9SAndreas Gohr        return $out;
9777e00bf9SAndreas Gohr    }
9877e00bf9SAndreas Gohr
9977e00bf9SAndreas Gohr    /**
10077e00bf9SAndreas Gohr     * Checks if the the CAPTCHA was solved correctly
10177e00bf9SAndreas Gohr     *
10277e00bf9SAndreas Gohr     * @param  bool $msg when true, an error will be signalled through the msg() method
10377e00bf9SAndreas Gohr     * @return bool true when the answer was correct, otherwise false
10477e00bf9SAndreas Gohr     */
10577e00bf9SAndreas Gohr    function check($msg=true){
10677e00bf9SAndreas Gohr        // compare provided string with decrypted captcha
10723d379a8SAndreas Gohr        $rand = PMA_blowfish_decrypt($_REQUEST[$this->field_sec],auth_cookiesalt());
1089e312724SAndreas Gohr
1099e312724SAndreas Gohr        if($this->getConf('mode') == 'math'){
1109e312724SAndreas Gohr            $code = $this->_generateMATH($this->_fixedIdent(),$rand);
1119e312724SAndreas Gohr            $code = $code[1];
1129e312724SAndreas Gohr        }else{
11377e00bf9SAndreas Gohr            $code = $this->_generateCAPTCHA($this->_fixedIdent(),$rand);
1149e312724SAndreas Gohr        }
11577e00bf9SAndreas Gohr
11623d379a8SAndreas Gohr        if(!$_REQUEST[$this->field_sec] ||
11723d379a8SAndreas Gohr           !$_REQUEST[$this->field_in] ||
11823d379a8SAndreas Gohr           strtoupper($_REQUEST[$this->field_in]) != $code ||
11923d379a8SAndreas Gohr           trim($_REQUEST[$this->field_hp]) !== ''
12023d379a8SAndreas Gohr          ){
12177e00bf9SAndreas Gohr            if($msg) msg($this->getLang('testfailed'),-1);
12277e00bf9SAndreas Gohr            return false;
12377e00bf9SAndreas Gohr        }
12477e00bf9SAndreas Gohr        return true;
12577e00bf9SAndreas Gohr    }
12677e00bf9SAndreas Gohr
12777e00bf9SAndreas Gohr    /**
12877e00bf9SAndreas Gohr     * Build a semi-secret fixed string identifying the current page and user
12977e00bf9SAndreas Gohr     *
13077e00bf9SAndreas Gohr     * This string is always the same for the current user when editing the same
131*27d84d8dSAndreas Gohr     * page revision, but only for one day. Editing a page before midnight and saving
132*27d84d8dSAndreas Gohr     * after midnight will result in a failed CAPTCHA once, but makes sure it can
133*27d84d8dSAndreas Gohr     * not be reused which is especially important for the registration form where the
134*27d84d8dSAndreas Gohr     * $ID usually won't change.
13577e00bf9SAndreas Gohr     */
13677e00bf9SAndreas Gohr    function _fixedIdent(){
13777e00bf9SAndreas Gohr        global $ID;
13877e00bf9SAndreas Gohr        $lm = @filemtime(wikiFN($ID));
139*27d84d8dSAndreas Gohr        $td = date('Y-m-d');
14077e00bf9SAndreas Gohr        return auth_browseruid().
14177e00bf9SAndreas Gohr               auth_cookiesalt().
142*27d84d8dSAndreas Gohr               $ID.$lm.$td;
14377e00bf9SAndreas Gohr    }
14477e00bf9SAndreas Gohr
14577e00bf9SAndreas Gohr    /**
14677e00bf9SAndreas Gohr     * Generates a random 5 char string
14777e00bf9SAndreas Gohr     *
14877e00bf9SAndreas Gohr     * @param $fixed string - the fixed part, any string
14977e00bf9SAndreas Gohr     * @param $rand  float  - some random number between 0 and 1
15023d379a8SAndreas Gohr     * @return string
15177e00bf9SAndreas Gohr     */
15277e00bf9SAndreas Gohr    function _generateCAPTCHA($fixed,$rand){
15377e00bf9SAndreas Gohr        $fixed = hexdec(substr(md5($fixed),5,5)); // use part of the md5 to generate an int
15477e00bf9SAndreas Gohr        $numbers = md5($rand * $fixed); // combine both values
15577e00bf9SAndreas Gohr
15677e00bf9SAndreas Gohr        // now create the letters
15777e00bf9SAndreas Gohr        $code = '';
15877e00bf9SAndreas Gohr        for($i=0;$i<10;$i+=2){
15977e00bf9SAndreas Gohr            $code .= chr(floor(hexdec($numbers[$i].$numbers[$i+1])/10) + 65);
16077e00bf9SAndreas Gohr        }
16177e00bf9SAndreas Gohr
16277e00bf9SAndreas Gohr        return $code;
16377e00bf9SAndreas Gohr    }
16477e00bf9SAndreas Gohr
1659e312724SAndreas Gohr    /**
1669e312724SAndreas Gohr     * Create a mathematical task and its result
1679e312724SAndreas Gohr     *
1689e312724SAndreas Gohr     * @param $fixed string - the fixed part, any string
1699e312724SAndreas Gohr     * @param $rand  float  - some random number between 0 and 1
1709e312724SAndreas Gohr     * @return string
1719e312724SAndreas Gohr     */
1729e312724SAndreas Gohr    function _generateMATH($fixed, $rand){
1739e312724SAndreas Gohr        $fixed = hexdec(substr(md5($fixed),5,5)); // use part of the md5 to generate an int
1749e312724SAndreas Gohr        $numbers = md5($rand * $fixed); // combine both values
1759e312724SAndreas Gohr
1769e312724SAndreas Gohr        // first letter is the operator (+/-)
1779e312724SAndreas Gohr        $op  = (hexdec($numbers[0]) > 8 ) ? -1 : 1;
1789e312724SAndreas Gohr        $num = array(hexdec($numbers[1].$numbers[2]), hexdec($numbers[3]));
1799e312724SAndreas Gohr
1809e312724SAndreas Gohr        // we only want positive results
1819e312724SAndreas Gohr        if(($op < 0) && ($num[0] < $num[1])) rsort($num);
1829e312724SAndreas Gohr
1839e312724SAndreas Gohr        // prepare result and task text
1849e312724SAndreas Gohr        $res  = $num[0] + ($num[1] * $op);
1859e312724SAndreas Gohr        $task = $num[0] . (($op < 0) ? '&nbsp;-&nbsp;' : '&nbsp;+&nbsp;') . $num[1] . '&nbsp;=&nbsp;?';
1869e312724SAndreas Gohr
1879e312724SAndreas Gohr        return array($task, $res);
1889e312724SAndreas Gohr    }
18977e00bf9SAndreas Gohr
19077e00bf9SAndreas Gohr    /**
19177e00bf9SAndreas Gohr     * Create a CAPTCHA image
19277e00bf9SAndreas Gohr     */
19377e00bf9SAndreas Gohr    function _imageCAPTCHA($text){
19477e00bf9SAndreas Gohr        $w = $this->getConf('width');
19577e00bf9SAndreas Gohr        $h = $this->getConf('height');
19677e00bf9SAndreas Gohr
197dc091fd0SAndreas Gohr        $fonts = glob(dirname(__FILE__).'/fonts/*.ttf');
198dc091fd0SAndreas Gohr
19977e00bf9SAndreas Gohr        // create a white image
20077e00bf9SAndreas Gohr        $img = imagecreate($w, $h);
20177e00bf9SAndreas Gohr        imagecolorallocate($img, 255, 255, 255);
20277e00bf9SAndreas Gohr
20377e00bf9SAndreas Gohr        // add some lines as background noise
20477e00bf9SAndreas Gohr        for ($i = 0; $i < 30; $i++) {
20577e00bf9SAndreas Gohr            $color = imagecolorallocate($img,rand(100, 250),rand(100, 250),rand(100, 250));
20677e00bf9SAndreas Gohr            imageline($img,rand(0,$w),rand(0,$h),rand(0,$w),rand(0,$h),$color);
20777e00bf9SAndreas Gohr        }
20877e00bf9SAndreas Gohr
20977e00bf9SAndreas Gohr        // draw the letters
21077e00bf9SAndreas Gohr        for ($i = 0; $i < strlen($text); $i++){
211dc091fd0SAndreas Gohr            $font  = $fonts[array_rand($fonts)];
21277e00bf9SAndreas Gohr            $color = imagecolorallocate($img, rand(0, 100), rand(0, 100), rand(0, 100));
21377e00bf9SAndreas Gohr            $size  = rand(floor($h/1.8),floor($h*0.7));
21477e00bf9SAndreas Gohr            $angle = rand(-35, 35);
21577e00bf9SAndreas Gohr
21677e00bf9SAndreas Gohr            $x = ($w*0.05) +  $i * floor($w*0.9/5);
21777e00bf9SAndreas Gohr            $cheight = $size + ($size*0.5);
21877e00bf9SAndreas Gohr            $y = floor($h / 2 + $cheight / 3.8);
21977e00bf9SAndreas Gohr
22077e00bf9SAndreas Gohr            imagettftext($img, $size, $angle, $x, $y, $color, $font, $text[$i]);
22177e00bf9SAndreas Gohr        }
22277e00bf9SAndreas Gohr
22377e00bf9SAndreas Gohr        header("Content-type: image/png");
22477e00bf9SAndreas Gohr        imagepng($img);
22577e00bf9SAndreas Gohr        imagedestroy($img);
22677e00bf9SAndreas Gohr    }
22777e00bf9SAndreas Gohr
22877e00bf9SAndreas Gohr
22977e00bf9SAndreas Gohr}
230