xref: /plugin/captcha/helper.php (revision 09b1e97e3cb9f2c4be8ca729baa9d49a3ba58ba1)
177e00bf9SAndreas Gohr<?php
2*09b1e97eSAndreas Gohr
3*09b1e97eSAndreas Gohruse dokuwiki\Extension\Plugin;
4*09b1e97eSAndreas Gohruse dokuwiki\Utf8\PhpString;
5*09b1e97eSAndreas Gohr
677e00bf9SAndreas Gohr/**
777e00bf9SAndreas Gohr * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
877e00bf9SAndreas Gohr * @author     Andreas Gohr <andi@splitbrain.org>
977e00bf9SAndreas Gohr */
107218f96cSAndreas Gohr
11a285df67SAndreas Gohr/**
12a285df67SAndreas Gohr * Class helper_plugin_captcha
13a285df67SAndreas Gohr */
14*09b1e97eSAndreas Gohrclass helper_plugin_captcha extends Plugin
1518622736SAndreas Gohr{
1623d379a8SAndreas Gohr    protected $field_in = 'plugin__captcha';
1723d379a8SAndreas Gohr    protected $field_sec = 'plugin__captcha_secret';
1823d379a8SAndreas Gohr    protected $field_hp = 'plugin__captcha_honeypot';
1923d379a8SAndreas Gohr
20*09b1e97eSAndreas Gohr    // region Public API
21*09b1e97eSAndreas Gohr
2223d379a8SAndreas Gohr    /**
2323d379a8SAndreas Gohr     * Constructor. Initializes field names
2423d379a8SAndreas Gohr     */
2518622736SAndreas Gohr    public function __construct()
2618622736SAndreas Gohr    {
27*09b1e97eSAndreas Gohr        $this->field_in = md5($this->fixedIdent() . $this->field_in);
28*09b1e97eSAndreas Gohr        $this->field_sec = md5($this->fixedIdent() . $this->field_sec);
29*09b1e97eSAndreas Gohr        $this->field_hp = md5($this->fixedIdent() . $this->field_hp);
3023d379a8SAndreas Gohr    }
3123d379a8SAndreas Gohr
3277e00bf9SAndreas Gohr    /**
3377e00bf9SAndreas Gohr     * Check if the CAPTCHA should be used. Always check this before using the methods below.
3477e00bf9SAndreas Gohr     *
3577e00bf9SAndreas Gohr     * @return bool true when the CAPTCHA should be used
3677e00bf9SAndreas Gohr     */
3718622736SAndreas Gohr    public function isEnabled()
3818622736SAndreas Gohr    {
3964382f29SAndreas Gohr        global $INPUT;
4064382f29SAndreas Gohr        if (!$this->getConf('forusers') && $INPUT->server->str('REMOTE_USER')) return false;
4177e00bf9SAndreas Gohr        return true;
4277e00bf9SAndreas Gohr    }
4377e00bf9SAndreas Gohr
4477e00bf9SAndreas Gohr    /**
4577e00bf9SAndreas Gohr     * Returns the HTML to display the CAPTCHA with the chosen method
46*09b1e97eSAndreas Gohr     *
47*09b1e97eSAndreas Gohr     * @return string The HTML to display the CAPTCHA
4877e00bf9SAndreas Gohr     */
4918622736SAndreas Gohr    public function getHTML()
5018622736SAndreas Gohr    {
5177e00bf9SAndreas Gohr        global $ID;
5277e00bf9SAndreas Gohr
53*09b1e97eSAndreas Gohr        $rand = (float)(random_int(0, 10000)) / 10000;
54*09b1e97eSAndreas Gohr        $this->storeCaptchaCookie($this->fixedIdent(), $rand);
55a285df67SAndreas Gohr
569e312724SAndreas Gohr        if ($this->getConf('mode') == 'math') {
57*09b1e97eSAndreas Gohr            $code = $this->generateMath($this->fixedIdent(), $rand);
589e312724SAndreas Gohr            $code = $code[0];
599e312724SAndreas Gohr            $text = $this->getLang('fillmath');
60df8afac4SAndreas Gohr        } elseif ($this->getConf('mode') == 'question') {
61a285df67SAndreas Gohr            $code = ''; // not used
62df8afac4SAndreas Gohr            $text = $this->getConf('question');
639e312724SAndreas Gohr        } else {
64*09b1e97eSAndreas Gohr            $code = $this->generateCaptchaCode($this->fixedIdent(), $rand);
659e312724SAndreas Gohr            $text = $this->getLang('fillcaptcha');
669e312724SAndreas Gohr        }
67f044313dSAndreas Gohr        $secret = $this->encrypt($rand);
6877e00bf9SAndreas Gohr
6928c14643SAndreas Gohr        $txtlen = $this->getConf('lettercount');
7028c14643SAndreas Gohr
7177e00bf9SAndreas Gohr        $out = '';
7277e00bf9SAndreas Gohr        $out .= '<div id="plugin__captcha_wrapper">';
7323d379a8SAndreas Gohr        $out .= '<input type="hidden" name="' . $this->field_sec . '" value="' . hsc($secret) . '" />';
749e312724SAndreas Gohr        $out .= '<label for="plugin__captcha">' . $text . '</label> ';
7523d379a8SAndreas Gohr
7677e00bf9SAndreas Gohr        switch ($this->getConf('mode')) {
779e312724SAndreas Gohr            case 'math':
789d6f09afSAndreas Gohr            case 'text':
79*09b1e97eSAndreas Gohr                $out .= $this->obfuscateText($code);
8077e00bf9SAndreas Gohr                break;
8177e00bf9SAndreas Gohr            case 'js':
82*09b1e97eSAndreas Gohr                $out .= '<span id="plugin__captcha_code">' . $this->obfuscateText($code) . '</span>';
8377e00bf9SAndreas Gohr                break;
8408f248e4SAndreas Gohr            case 'svg':
8508f248e4SAndreas Gohr                $out .= '<span class="svg" style="width:' . $this->getConf('width') . 'px; height:' . $this->getConf('height') . 'px">';
86*09b1e97eSAndreas Gohr                $out .= $this->svgCaptcha($code);
8708f248e4SAndreas Gohr                $out .= '</span>';
8808f248e4SAndreas Gohr                break;
8908f248e4SAndreas Gohr            case 'svgaudio':
9008f248e4SAndreas Gohr                $out .= '<span class="svg" style="width:' . $this->getConf('width') . 'px; height:' . $this->getConf('height') . 'px">';
91*09b1e97eSAndreas Gohr                $out .= $this->svgCaptcha($code);
9208f248e4SAndreas Gohr                $out .= '</span>';
9308f248e4SAndreas Gohr                $out .= '<a href="' . DOKU_BASE . 'lib/plugins/captcha/wav.php?secret=' . rawurlencode($secret) . '&amp;id=' . $ID . '"' .
949efb703bSStefan Bethke                    ' class="JSnocheck audiolink" title="' . $this->getLang('soundlink') . '">';
9508f248e4SAndreas Gohr                $out .= '<img src="' . DOKU_BASE . 'lib/plugins/captcha/sound.png" width="16" height="16"' .
9608f248e4SAndreas Gohr                    ' alt="' . $this->getLang('soundlink') . '" /></a>';
9708f248e4SAndreas Gohr                break;
9877e00bf9SAndreas Gohr            case 'image':
9977e00bf9SAndreas Gohr                $out .= '<img src="' . DOKU_BASE . 'lib/plugins/captcha/img.php?secret=' . rawurlencode($secret) . '&amp;id=' . $ID . '" ' .
10077e00bf9SAndreas Gohr                    ' width="' . $this->getConf('width') . '" height="' . $this->getConf('height') . '" alt="" /> ';
10177e00bf9SAndreas Gohr                break;
10277e00bf9SAndreas Gohr            case 'audio':
10377e00bf9SAndreas Gohr                $out .= '<img src="' . DOKU_BASE . 'lib/plugins/captcha/img.php?secret=' . rawurlencode($secret) . '&amp;id=' . $ID . '" ' .
10477e00bf9SAndreas Gohr                    ' width="' . $this->getConf('width') . '" height="' . $this->getConf('height') . '" alt="" /> ';
10577e00bf9SAndreas Gohr                $out .= '<a href="' . DOKU_BASE . 'lib/plugins/captcha/wav.php?secret=' . rawurlencode($secret) . '&amp;id=' . $ID . '"' .
1069efb703bSStefan Bethke                    ' class="JSnocheck audiolink" title="' . $this->getLang('soundlink') . '">';
10777e00bf9SAndreas Gohr                $out .= '<img src="' . DOKU_BASE . 'lib/plugins/captcha/sound.png" width="16" height="16"' .
10877e00bf9SAndreas Gohr                    ' alt="' . $this->getLang('soundlink') . '" /></a>';
10977e00bf9SAndreas Gohr                break;
11052e95008SAndreas Gohr            case 'figlet':
111*09b1e97eSAndreas Gohr                require_once(__DIR__ . '/figlet.php');
11252e95008SAndreas Gohr                $figlet = new phpFiglet();
113*09b1e97eSAndreas Gohr                if ($figlet->loadfont(__DIR__ . '/figlet.flf')) {
11452e95008SAndreas Gohr                    $out .= '<pre>';
11552e95008SAndreas Gohr                    $out .= rtrim($figlet->fetch($code));
11652e95008SAndreas Gohr                    $out .= '</pre>';
11752e95008SAndreas Gohr                } else {
11852e95008SAndreas Gohr                    msg('Failed to load figlet.flf font file. CAPTCHA broken', -1);
11952e95008SAndreas Gohr                }
12052e95008SAndreas Gohr                break;
12177e00bf9SAndreas Gohr        }
122df8afac4SAndreas Gohr        $out .= ' <input type="text" size="' . $txtlen . '" name="' . $this->field_in . '" class="edit" /> ';
12323d379a8SAndreas Gohr
12423d379a8SAndreas Gohr        // add honeypot field
1259d63c05fSlainme        $out .= '<label class="no">' . $this->getLang('honeypot') . '<input type="text" name="' . $this->field_hp . '" /></label>';
12677e00bf9SAndreas Gohr        $out .= '</div>';
12777e00bf9SAndreas Gohr        return $out;
12877e00bf9SAndreas Gohr    }
12977e00bf9SAndreas Gohr
13077e00bf9SAndreas Gohr    /**
13118622736SAndreas Gohr     * Checks if the CAPTCHA was solved correctly
13277e00bf9SAndreas Gohr     *
13377e00bf9SAndreas Gohr     * @param bool $msg when true, an error will be signalled through the msg() method
13477e00bf9SAndreas Gohr     * @return bool true when the answer was correct, otherwise false
13577e00bf9SAndreas Gohr     */
13618622736SAndreas Gohr    public function check($msg = true)
13718622736SAndreas Gohr    {
138478e363cSAndreas Gohr        global $INPUT;
139478e363cSAndreas Gohr
140478e363cSAndreas Gohr        $field_sec = $INPUT->str($this->field_sec);
141478e363cSAndreas Gohr        $field_in = $INPUT->str($this->field_in);
142478e363cSAndreas Gohr        $field_hp = $INPUT->str($this->field_hp);
143478e363cSAndreas Gohr
144478e363cSAndreas Gohr        // reconstruct captcha from provided $field_sec
145478e363cSAndreas Gohr        $rand = $this->decrypt($field_sec);
1469e312724SAndreas Gohr
1479e312724SAndreas Gohr        if ($this->getConf('mode') == 'math') {
148*09b1e97eSAndreas Gohr            $code = $this->generateMath($this->fixedIdent(), $rand);
1499e312724SAndreas Gohr            $code = $code[1];
150df8afac4SAndreas Gohr        } elseif ($this->getConf('mode') == 'question') {
151df8afac4SAndreas Gohr            $code = $this->getConf('answer');
1529e312724SAndreas Gohr        } else {
153*09b1e97eSAndreas Gohr            $code = $this->generateCaptchaCode($this->fixedIdent(), $rand);
1549e312724SAndreas Gohr        }
15577e00bf9SAndreas Gohr
156478e363cSAndreas Gohr        // compare values
157*09b1e97eSAndreas Gohr        if (
158*09b1e97eSAndreas Gohr            !$field_sec ||
159478e363cSAndreas Gohr            !$field_in ||
16014e271ebSPatrick Brown            $rand === false ||
161*09b1e97eSAndreas Gohr            PhpString::strtolower($field_in) != PhpString::strtolower($code) ||
162a285df67SAndreas Gohr            trim($field_hp) !== '' ||
163*09b1e97eSAndreas Gohr            !$this->retrieveCaptchaCookie($this->fixedIdent(), $rand)
16423d379a8SAndreas Gohr        ) {
16577e00bf9SAndreas Gohr            if ($msg) msg($this->getLang('testfailed'), -1);
16677e00bf9SAndreas Gohr            return false;
16777e00bf9SAndreas Gohr        }
16877e00bf9SAndreas Gohr        return true;
16977e00bf9SAndreas Gohr    }
17077e00bf9SAndreas Gohr
171*09b1e97eSAndreas Gohr    // endregion
172*09b1e97eSAndreas Gohr
173*09b1e97eSAndreas Gohr    // region Captcha Cookie methods
174*09b1e97eSAndreas Gohr
17577e00bf9SAndreas Gohr    /**
176cde3ece1SAndreas Gohr     * Get the path where a captcha cookie would be stored
177a285df67SAndreas Gohr     *
178a285df67SAndreas Gohr     * We use a daily temp directory which is easy to clean up
179a285df67SAndreas Gohr     *
180a285df67SAndreas Gohr     * @param $fixed string the fixed part, any string
181a285df67SAndreas Gohr     * @param $rand  float  some random number between 0 and 1
182a285df67SAndreas Gohr     * @return string the path to the cookie file
183a285df67SAndreas Gohr     */
18418622736SAndreas Gohr    protected function getCaptchaCookiePath($fixed, $rand)
18518622736SAndreas Gohr    {
186a285df67SAndreas Gohr        global $conf;
187a285df67SAndreas Gohr        $path = $conf['tmpdir'] . '/captcha/' . date('Y-m-d') . '/' . md5($fixed . $rand) . '.cookie';
188a285df67SAndreas Gohr        io_makeFileDir($path);
189a285df67SAndreas Gohr        return $path;
190a285df67SAndreas Gohr    }
191a285df67SAndreas Gohr
192a285df67SAndreas Gohr    /**
193cde3ece1SAndreas Gohr     * remove all outdated captcha cookies
194cde3ece1SAndreas Gohr     */
195*09b1e97eSAndreas Gohr    public function cleanCaptchaCookies()
19618622736SAndreas Gohr    {
197cde3ece1SAndreas Gohr        global $conf;
198cde3ece1SAndreas Gohr        $path = $conf['tmpdir'] . '/captcha/';
199cde3ece1SAndreas Gohr        $dirs = glob("$path/*", GLOB_ONLYDIR);
200cde3ece1SAndreas Gohr        $today = date('Y-m-d');
201cde3ece1SAndreas Gohr        foreach ($dirs as $dir) {
202cde3ece1SAndreas Gohr            if (basename($dir) === $today) continue;
203cde3ece1SAndreas Gohr            if (!preg_match('/\/captcha\//', $dir)) continue; // safety net
204cde3ece1SAndreas Gohr            io_rmdir($dir, true);
205cde3ece1SAndreas Gohr        }
206cde3ece1SAndreas Gohr    }
207cde3ece1SAndreas Gohr
208cde3ece1SAndreas Gohr    /**
209a285df67SAndreas Gohr     * Creates a one time captcha cookie
210a285df67SAndreas Gohr     *
211a285df67SAndreas Gohr     * This is used to prevent replay attacks. It is generated when the captcha form
212a285df67SAndreas Gohr     * is shown and checked with the captcha check. Since we can not be sure about the
213a285df67SAndreas Gohr     * session state (might be closed or open) we're not using it.
214a285df67SAndreas Gohr     *
215a285df67SAndreas Gohr     * We're not using the stored values for displaying the captcha image (or audio)
216a285df67SAndreas Gohr     * but continue to use our encryption scheme. This way it's still possible to have
217a285df67SAndreas Gohr     * multiple captcha checks going on in parallel (eg. with multiple browser tabs)
218a285df67SAndreas Gohr     *
219a285df67SAndreas Gohr     * @param $fixed string the fixed part, any string
220a285df67SAndreas Gohr     * @param $rand  float  some random number between 0 and 1
221a285df67SAndreas Gohr     */
22218622736SAndreas Gohr    protected function storeCaptchaCookie($fixed, $rand)
22318622736SAndreas Gohr    {
224a285df67SAndreas Gohr        $cache = $this->getCaptchaCookiePath($fixed, $rand);
225a285df67SAndreas Gohr        touch($cache);
226a285df67SAndreas Gohr    }
227a285df67SAndreas Gohr
228a285df67SAndreas Gohr    /**
229a285df67SAndreas Gohr     * Checks if the captcha cookie exists and deletes it
230a285df67SAndreas Gohr     *
231a285df67SAndreas Gohr     * @param $fixed string the fixed part, any string
232a285df67SAndreas Gohr     * @param $rand  float  some random number between 0 and 1
233a285df67SAndreas Gohr     * @return bool true if the cookie existed
234a285df67SAndreas Gohr     */
23518622736SAndreas Gohr    protected function retrieveCaptchaCookie($fixed, $rand)
23618622736SAndreas Gohr    {
237a285df67SAndreas Gohr        $cache = $this->getCaptchaCookiePath($fixed, $rand);
238a285df67SAndreas Gohr        if (file_exists($cache)) {
239a285df67SAndreas Gohr            unlink($cache);
240a285df67SAndreas Gohr            return true;
241a285df67SAndreas Gohr        }
242a285df67SAndreas Gohr        return false;
243a285df67SAndreas Gohr    }
244a285df67SAndreas Gohr
245*09b1e97eSAndreas Gohr    // endregion
246*09b1e97eSAndreas Gohr
247*09b1e97eSAndreas Gohr    // region Captcha Generation methods
248*09b1e97eSAndreas Gohr
249a285df67SAndreas Gohr    /**
25077e00bf9SAndreas Gohr     * Build a semi-secret fixed string identifying the current page and user
25177e00bf9SAndreas Gohr     *
25277e00bf9SAndreas Gohr     * This string is always the same for the current user when editing the same
25327d84d8dSAndreas Gohr     * page revision, but only for one day. Editing a page before midnight and saving
25427d84d8dSAndreas Gohr     * after midnight will result in a failed CAPTCHA once, but makes sure it can
25527d84d8dSAndreas Gohr     * not be reused which is especially important for the registration form where the
25627d84d8dSAndreas Gohr     * $ID usually won't change.
257104ec268SAndreas Gohr     *
258104ec268SAndreas Gohr     * @return string
25977e00bf9SAndreas Gohr     */
260*09b1e97eSAndreas Gohr    public function fixedIdent()
26118622736SAndreas Gohr    {
26277e00bf9SAndreas Gohr        global $ID;
26377e00bf9SAndreas Gohr        $lm = @filemtime(wikiFN($ID));
26427d84d8dSAndreas Gohr        $td = date('Y-m-d');
26563609b6eSAndreas Gohr        $ip = clientIP();
26663609b6eSAndreas Gohr        $salt = auth_cookiesalt();
26763609b6eSAndreas Gohr
268*09b1e97eSAndreas Gohr        return sha1(implode("\n", [$ID, $lm, $td, $ip, $salt]));
26977e00bf9SAndreas Gohr    }
27077e00bf9SAndreas Gohr
27177e00bf9SAndreas Gohr    /**
272*09b1e97eSAndreas Gohr     * Generate a magic code based on the given data
2739d6f09afSAndreas Gohr     *
274*09b1e97eSAndreas Gohr     * This "magic" code represents the given fixed identifier (see fixedIdent()) and the given
275*09b1e97eSAndreas Gohr     * random number. It is used to generate the actual CAPTCHA code.
2769d6f09afSAndreas Gohr     *
277*09b1e97eSAndreas Gohr     * @param $ident string the fixed part, any string
278a02b2219SPatrick Brown     * @param $rand  float  some random number between 0 and 1
279a02b2219SPatrick Brown     * @return string
280a02b2219SPatrick Brown     */
281*09b1e97eSAndreas Gohr    protected function generateMagicCode($ident, $rand)
28218622736SAndreas Gohr    {
283*09b1e97eSAndreas Gohr        $ident = hexdec(substr(md5($ident), 5, 5)); // use part of the md5 to generate an int
284*09b1e97eSAndreas Gohr        $rand *= 0xFFFFF; // bitmask from the random number
285*09b1e97eSAndreas Gohr        return md5($rand ^ $ident); // combine both values
286a02b2219SPatrick Brown    }
287a02b2219SPatrick Brown
288a02b2219SPatrick Brown    /**
289*09b1e97eSAndreas Gohr     * Generates a char string based on the given data
29077e00bf9SAndreas Gohr     *
291*09b1e97eSAndreas Gohr     * The string is pseudo random based on a fixed identifier (see fixedIdent()) and a random number.
292*09b1e97eSAndreas Gohr     *
293*09b1e97eSAndreas Gohr     * @param $ident string the fixed part, any string
294104ec268SAndreas Gohr     * @param $rand  float  some random number between 0 and 1
29523d379a8SAndreas Gohr     * @return string
29677e00bf9SAndreas Gohr     */
297*09b1e97eSAndreas Gohr    public function generateCaptchaCode($ident, $rand)
29818622736SAndreas Gohr    {
299*09b1e97eSAndreas Gohr        $numbers = $this->generateMagicCode($ident, $rand);
30077e00bf9SAndreas Gohr
30177e00bf9SAndreas Gohr        // now create the letters
30277e00bf9SAndreas Gohr        $code = '';
3039a516edaSPatrick Brown        $lettercount = $this->getConf('lettercount') * 2;
3049a516edaSPatrick Brown        if ($lettercount > strlen($numbers)) $lettercount = strlen($numbers);
3059a516edaSPatrick Brown        for ($i = 0; $i < $lettercount; $i += 2) {
30677e00bf9SAndreas Gohr            $code .= chr(floor(hexdec($numbers[$i] . $numbers[$i + 1]) / 10) + 65);
30777e00bf9SAndreas Gohr        }
30877e00bf9SAndreas Gohr
30977e00bf9SAndreas Gohr        return $code;
31077e00bf9SAndreas Gohr    }
31177e00bf9SAndreas Gohr
3129e312724SAndreas Gohr    /**
3139e312724SAndreas Gohr     * Create a mathematical task and its result
3149e312724SAndreas Gohr     *
315*09b1e97eSAndreas Gohr     * @param $ident string the fixed part, any string
316104ec268SAndreas Gohr     * @param $rand  float  some random number between 0 and 1
317*09b1e97eSAndreas Gohr     * @return array [task, result]
3189e312724SAndreas Gohr     */
319*09b1e97eSAndreas Gohr    protected function generateMath($ident, $rand)
32018622736SAndreas Gohr    {
321*09b1e97eSAndreas Gohr        $numbers = $this->generateMagicCode($ident, $rand);
3229e312724SAndreas Gohr
3239e312724SAndreas Gohr        // first letter is the operator (+/-)
3249e312724SAndreas Gohr        $op = (hexdec($numbers[0]) > 8) ? -1 : 1;
325*09b1e97eSAndreas Gohr        $num = [hexdec($numbers[1] . $numbers[2]), hexdec($numbers[3])];
3269e312724SAndreas Gohr
3279e312724SAndreas Gohr        // we only want positive results
3289e312724SAndreas Gohr        if (($op < 0) && ($num[0] < $num[1])) rsort($num);
3299e312724SAndreas Gohr
3309e312724SAndreas Gohr        // prepare result and task text
3319e312724SAndreas Gohr        $res = $num[0] + ($num[1] * $op);
3329bc1fab2SApostolos P. Tsompanopoulos        $task = $num[0] . (($op < 0) ? '-' : '+') . $num[1] . '= ';
3339e312724SAndreas Gohr
334*09b1e97eSAndreas Gohr        return [$task, $res];
3359e312724SAndreas Gohr    }
33677e00bf9SAndreas Gohr
337*09b1e97eSAndreas Gohr    // endregion
338*09b1e97eSAndreas Gohr
339*09b1e97eSAndreas Gohr    // region Output Builders
340*09b1e97eSAndreas Gohr
34177e00bf9SAndreas Gohr    /**
34277e00bf9SAndreas Gohr     * Create a CAPTCHA image
343104ec268SAndreas Gohr     *
344104ec268SAndreas Gohr     * @param string $text the letters to display
345*09b1e97eSAndreas Gohr     * @return string The image data
34677e00bf9SAndreas Gohr     */
347*09b1e97eSAndreas Gohr    public function imageCaptcha($text)
34818622736SAndreas Gohr    {
34977e00bf9SAndreas Gohr        $w = $this->getConf('width');
35077e00bf9SAndreas Gohr        $h = $this->getConf('height');
35177e00bf9SAndreas Gohr
352*09b1e97eSAndreas Gohr        $fonts = glob(__DIR__ . '/fonts/*.ttf');
353dc091fd0SAndreas Gohr
35477e00bf9SAndreas Gohr        // create a white image
35528c14643SAndreas Gohr        $img = imagecreatetruecolor($w, $h);
35628c14643SAndreas Gohr        $white = imagecolorallocate($img, 255, 255, 255);
35728c14643SAndreas Gohr        imagefill($img, 0, 0, $white);
35877e00bf9SAndreas Gohr
35977e00bf9SAndreas Gohr        // add some lines as background noise
36077e00bf9SAndreas Gohr        for ($i = 0; $i < 30; $i++) {
361*09b1e97eSAndreas Gohr            $color = imagecolorallocate($img, random_int(100, 250), random_int(100, 250), random_int(100, 250));
362*09b1e97eSAndreas Gohr            imageline($img, random_int(0, $w), random_int(0, $h), random_int(0, $w), random_int(0, $h), $color);
36377e00bf9SAndreas Gohr        }
36477e00bf9SAndreas Gohr
36577e00bf9SAndreas Gohr        // draw the letters
36628c14643SAndreas Gohr        $txtlen = strlen($text);
36728c14643SAndreas Gohr        for ($i = 0; $i < $txtlen; $i++) {
368dc091fd0SAndreas Gohr            $font = $fonts[array_rand($fonts)];
369*09b1e97eSAndreas Gohr            $color = imagecolorallocate($img, random_int(0, 100), random_int(0, 100), random_int(0, 100));
370*09b1e97eSAndreas Gohr            $size = random_int(floor($h / 1.8), floor($h * 0.7));
371*09b1e97eSAndreas Gohr            $angle = random_int(-35, 35);
37277e00bf9SAndreas Gohr
37328c14643SAndreas Gohr            $x = ($w * 0.05) + $i * floor($w * 0.9 / $txtlen);
37477e00bf9SAndreas Gohr            $cheight = $size + ($size * 0.5);
37577e00bf9SAndreas Gohr            $y = floor($h / 2 + $cheight / 3.8);
37677e00bf9SAndreas Gohr
37777e00bf9SAndreas Gohr            imagettftext($img, $size, $angle, $x, $y, $color, $font, $text[$i]);
37877e00bf9SAndreas Gohr        }
37977e00bf9SAndreas Gohr
380*09b1e97eSAndreas Gohr        ob_start();
38177e00bf9SAndreas Gohr        imagepng($img);
382*09b1e97eSAndreas Gohr        $image = ob_get_clean();
38377e00bf9SAndreas Gohr        imagedestroy($img);
384*09b1e97eSAndreas Gohr        return $image;
38508f248e4SAndreas Gohr    }
38608f248e4SAndreas Gohr
38708f248e4SAndreas Gohr    /**
38863609b6eSAndreas Gohr     * Generate an audio captcha
38963609b6eSAndreas Gohr     *
39063609b6eSAndreas Gohr     * @param string $text
391*09b1e97eSAndreas Gohr     * @return string The joined wav files
39263609b6eSAndreas Gohr     */
393*09b1e97eSAndreas Gohr    public function audioCaptcha($text)
394*09b1e97eSAndreas Gohr    {
39563609b6eSAndreas Gohr        global $conf;
39663609b6eSAndreas Gohr
39763609b6eSAndreas Gohr        $lc = __DIR__ . '/lang/' . $conf['lang'] . '/audio/';
39863609b6eSAndreas Gohr        $en = __DIR__ . '/lang/en/audio/';
39963609b6eSAndreas Gohr
40063609b6eSAndreas Gohr        $wavs = [];
40163609b6eSAndreas Gohr
40263609b6eSAndreas Gohr        $text = strtolower($text);
40363609b6eSAndreas Gohr        $txtlen = strlen($text);
40463609b6eSAndreas Gohr        for ($i = 0; $i < $txtlen; $i++) {
40563609b6eSAndreas Gohr            $char = $text[$i];
40663609b6eSAndreas Gohr            $file = $lc . $char . '.wav';
40763609b6eSAndreas Gohr            if (!@file_exists($file)) $file = $en . $char . '.wav';
40863609b6eSAndreas Gohr            $wavs[] = $file;
40963609b6eSAndreas Gohr        }
41063609b6eSAndreas Gohr
411*09b1e97eSAndreas Gohr        return $this->joinwavs($wavs);
41263609b6eSAndreas Gohr    }
41363609b6eSAndreas Gohr
41463609b6eSAndreas Gohr    /**
415*09b1e97eSAndreas Gohr     * Create an SVG of the given text
416*09b1e97eSAndreas Gohr     *
417*09b1e97eSAndreas Gohr     * @param string $text
418*09b1e97eSAndreas Gohr     * @return string
419*09b1e97eSAndreas Gohr     */
420*09b1e97eSAndreas Gohr    public function svgCaptcha($text)
421*09b1e97eSAndreas Gohr    {
422*09b1e97eSAndreas Gohr        require_once(__DIR__ . '/EasySVG.php');
423*09b1e97eSAndreas Gohr
424*09b1e97eSAndreas Gohr        $fonts = glob(__DIR__ . '/fonts/*.svg');
425*09b1e97eSAndreas Gohr
426*09b1e97eSAndreas Gohr        $x = 0; // where we start to draw
427*09b1e97eSAndreas Gohr        $y = 100; // our max height
428*09b1e97eSAndreas Gohr
429*09b1e97eSAndreas Gohr        $svg = new EasySVG();
430*09b1e97eSAndreas Gohr
431*09b1e97eSAndreas Gohr        // draw the letters
432*09b1e97eSAndreas Gohr        $txtlen = strlen($text);
433*09b1e97eSAndreas Gohr        for ($i = 0; $i < $txtlen; $i++) {
434*09b1e97eSAndreas Gohr            $char = $text[$i];
435*09b1e97eSAndreas Gohr            $size = random_int($y / 2, $y - $y * 0.1); // 50-90%
436*09b1e97eSAndreas Gohr            $svg->setFontSVG($fonts[array_rand($fonts)]);
437*09b1e97eSAndreas Gohr
438*09b1e97eSAndreas Gohr            $svg->setFontSize($size);
439*09b1e97eSAndreas Gohr            $svg->setLetterSpacing(round(random_int(1, 4) / 10, 2)); // 0.1 - 0.4
440*09b1e97eSAndreas Gohr            $svg->addText($char, $x, random_int(0, round($y - $size))); // random up and down
441*09b1e97eSAndreas Gohr
442*09b1e97eSAndreas Gohr            [$w] = $svg->textDimensions($char);
443*09b1e97eSAndreas Gohr            $x += $w;
444*09b1e97eSAndreas Gohr        }
445*09b1e97eSAndreas Gohr
446*09b1e97eSAndreas Gohr        $svg->addAttribute('width', $x . 'px');
447*09b1e97eSAndreas Gohr        $svg->addAttribute('height', $y . 'px');
448*09b1e97eSAndreas Gohr        $svg->addAttribute('viewbox', "0 0 $x $y");
449*09b1e97eSAndreas Gohr        return $svg->asXML();
450*09b1e97eSAndreas Gohr    }
451*09b1e97eSAndreas Gohr
452*09b1e97eSAndreas Gohr    // endregion
453*09b1e97eSAndreas Gohr
454*09b1e97eSAndreas Gohr    // region Utilities
455*09b1e97eSAndreas Gohr
456*09b1e97eSAndreas Gohr    /**
457f044313dSAndreas Gohr     * Encrypt the given string with the cookie salt
458f044313dSAndreas Gohr     *
459f044313dSAndreas Gohr     * @param string $data
460f044313dSAndreas Gohr     * @return string
461f044313dSAndreas Gohr     */
46218622736SAndreas Gohr    public function encrypt($data)
46318622736SAndreas Gohr    {
464*09b1e97eSAndreas Gohr        $data = auth_encrypt($data, auth_cookiesalt());
465f044313dSAndreas Gohr        return base64_encode($data);
466f044313dSAndreas Gohr    }
467f044313dSAndreas Gohr
468f044313dSAndreas Gohr    /**
469f044313dSAndreas Gohr     * Decrypt the given string with the cookie salt
470f044313dSAndreas Gohr     *
471f044313dSAndreas Gohr     * @param string $data
472f044313dSAndreas Gohr     * @return string
473f044313dSAndreas Gohr     */
47418622736SAndreas Gohr    public function decrypt($data)
47518622736SAndreas Gohr    {
476f044313dSAndreas Gohr        $data = base64_decode($data);
47709870f99SPatrick Brown        if ($data === false || $data === '') return false;
478f044313dSAndreas Gohr
479*09b1e97eSAndreas Gohr        return auth_decrypt($data, auth_cookiesalt());
480f044313dSAndreas Gohr    }
481*09b1e97eSAndreas Gohr
482*09b1e97eSAndreas Gohr    /**
483*09b1e97eSAndreas Gohr     * Adds random space characters within the given text
484*09b1e97eSAndreas Gohr     *
485*09b1e97eSAndreas Gohr     * Keeps subsequent numbers without spaces (for math problem)
486*09b1e97eSAndreas Gohr     *
487*09b1e97eSAndreas Gohr     * @param $text
488*09b1e97eSAndreas Gohr     * @return string
489*09b1e97eSAndreas Gohr     */
490*09b1e97eSAndreas Gohr    protected function obfuscateText($text)
491*09b1e97eSAndreas Gohr    {
492*09b1e97eSAndreas Gohr        $new = '';
493*09b1e97eSAndreas Gohr
494*09b1e97eSAndreas Gohr        $spaces = [
495*09b1e97eSAndreas Gohr            "\r",
496*09b1e97eSAndreas Gohr            "\n",
497*09b1e97eSAndreas Gohr            "\r\n",
498*09b1e97eSAndreas Gohr            ' ',
499*09b1e97eSAndreas Gohr            "\xC2\xA0",
500*09b1e97eSAndreas Gohr            // \u00A0    NO-BREAK SPACE
501*09b1e97eSAndreas Gohr            "\xE2\x80\x80",
502*09b1e97eSAndreas Gohr            // \u2000    EN QUAD
503*09b1e97eSAndreas Gohr            "\xE2\x80\x81",
504*09b1e97eSAndreas Gohr            // \u2001    EM QUAD
505*09b1e97eSAndreas Gohr            "\xE2\x80\x82",
506*09b1e97eSAndreas Gohr            // \u2002    EN SPACE
507*09b1e97eSAndreas Gohr            //         "\xE2\x80\x83", // \u2003    EM SPACE
508*09b1e97eSAndreas Gohr            "\xE2\x80\x84",
509*09b1e97eSAndreas Gohr            // \u2004    THREE-PER-EM SPACE
510*09b1e97eSAndreas Gohr            "\xE2\x80\x85",
511*09b1e97eSAndreas Gohr            // \u2005    FOUR-PER-EM SPACE
512*09b1e97eSAndreas Gohr            "\xE2\x80\x86",
513*09b1e97eSAndreas Gohr            // \u2006    SIX-PER-EM SPACE
514*09b1e97eSAndreas Gohr            "\xE2\x80\x87",
515*09b1e97eSAndreas Gohr            // \u2007    FIGURE SPACE
516*09b1e97eSAndreas Gohr            "\xE2\x80\x88",
517*09b1e97eSAndreas Gohr            // \u2008    PUNCTUATION SPACE
518*09b1e97eSAndreas Gohr            "\xE2\x80\x89",
519*09b1e97eSAndreas Gohr            // \u2009    THIN SPACE
520*09b1e97eSAndreas Gohr            "\xE2\x80\x8A",
521*09b1e97eSAndreas Gohr            // \u200A    HAIR SPACE
522*09b1e97eSAndreas Gohr            "\xE2\x80\xAF",
523*09b1e97eSAndreas Gohr            // \u202F    NARROW NO-BREAK SPACE
524*09b1e97eSAndreas Gohr            "\xE2\x81\x9F",
525*09b1e97eSAndreas Gohr            // \u205F    MEDIUM MATHEMATICAL SPACE
526*09b1e97eSAndreas Gohr            "\xE1\xA0\x8E\r\n",
527*09b1e97eSAndreas Gohr            // \u180E    MONGOLIAN VOWEL SEPARATOR
528*09b1e97eSAndreas Gohr            "\xE2\x80\x8B\r\n",
529*09b1e97eSAndreas Gohr            // \u200B    ZERO WIDTH SPACE
530*09b1e97eSAndreas Gohr            "\xEF\xBB\xBF\r\n",
531*09b1e97eSAndreas Gohr        ];
532*09b1e97eSAndreas Gohr
533*09b1e97eSAndreas Gohr        $len = strlen($text);
534*09b1e97eSAndreas Gohr        for ($i = 0; $i < $len - 1; $i++) {
535*09b1e97eSAndreas Gohr            $new .= $text[$i];
536*09b1e97eSAndreas Gohr
537*09b1e97eSAndreas Gohr            if (!is_numeric($text[$i + 1])) {
538*09b1e97eSAndreas Gohr                $new .= $spaces[array_rand($spaces)];
539*09b1e97eSAndreas Gohr            }
540*09b1e97eSAndreas Gohr        }
541*09b1e97eSAndreas Gohr        $new .= $text[$len - 1];
542*09b1e97eSAndreas Gohr        return $new;
543f044313dSAndreas Gohr    }
544969b14c4SAndreas Gohr
54563609b6eSAndreas Gohr
54663609b6eSAndreas Gohr    /**
54763609b6eSAndreas Gohr     * Join multiple wav files
54863609b6eSAndreas Gohr     *
54963609b6eSAndreas Gohr     * All wave files need to have the same format and need to be uncompressed.
55063609b6eSAndreas Gohr     * The headers of the last file will be used (with recalculated datasize
55163609b6eSAndreas Gohr     * of course)
55263609b6eSAndreas Gohr     *
55363609b6eSAndreas Gohr     * @link http://ccrma.stanford.edu/CCRMA/Courses/422/projects/WaveFormat/
55463609b6eSAndreas Gohr     * @link http://www.thescripts.com/forum/thread3770.html
55563609b6eSAndreas Gohr     */
55663609b6eSAndreas Gohr    protected function joinwavs($wavs)
55763609b6eSAndreas Gohr    {
558*09b1e97eSAndreas Gohr        $fields = implode(
559*09b1e97eSAndreas Gohr            '/',
560*09b1e97eSAndreas Gohr            [
56163609b6eSAndreas Gohr                'H8ChunkID',
56263609b6eSAndreas Gohr                'VChunkSize',
56363609b6eSAndreas Gohr                'H8Format',
56463609b6eSAndreas Gohr                'H8Subchunk1ID',
56563609b6eSAndreas Gohr                'VSubchunk1Size',
56663609b6eSAndreas Gohr                'vAudioFormat',
56763609b6eSAndreas Gohr                'vNumChannels',
56863609b6eSAndreas Gohr                'VSampleRate',
56963609b6eSAndreas Gohr                'VByteRate',
57063609b6eSAndreas Gohr                'vBlockAlign',
571*09b1e97eSAndreas Gohr                'vBitsPerSample'
572*09b1e97eSAndreas Gohr            ]
57363609b6eSAndreas Gohr        );
57463609b6eSAndreas Gohr
57563609b6eSAndreas Gohr        $data = '';
57663609b6eSAndreas Gohr        foreach ($wavs as $wav) {
57763609b6eSAndreas Gohr            $fp = fopen($wav, 'rb');
57863609b6eSAndreas Gohr            $header = fread($fp, 36);
57963609b6eSAndreas Gohr            $info = unpack($fields, $header);
58063609b6eSAndreas Gohr
58163609b6eSAndreas Gohr            // read optional extra stuff
58263609b6eSAndreas Gohr            if ($info['Subchunk1Size'] > 16) {
58363609b6eSAndreas Gohr                $header .= fread($fp, ($info['Subchunk1Size'] - 16));
58463609b6eSAndreas Gohr            }
58563609b6eSAndreas Gohr
58663609b6eSAndreas Gohr            // read SubChunk2ID
58763609b6eSAndreas Gohr            $header .= fread($fp, 4);
58863609b6eSAndreas Gohr
58963609b6eSAndreas Gohr            // read Subchunk2Size
59063609b6eSAndreas Gohr            $size = unpack('vsize', fread($fp, 4));
59163609b6eSAndreas Gohr            $size = $size['size'];
59263609b6eSAndreas Gohr
59363609b6eSAndreas Gohr            // read data
59463609b6eSAndreas Gohr            $data .= fread($fp, $size);
59563609b6eSAndreas Gohr        }
59663609b6eSAndreas Gohr
59763609b6eSAndreas Gohr        return $header . pack('V', strlen($data)) . $data;
59863609b6eSAndreas Gohr    }
59963609b6eSAndreas Gohr
600*09b1e97eSAndreas Gohr    // endregion
60177e00bf9SAndreas Gohr}
602