xref: /plugin/captcha/helper.php (revision 184a20604ec0bd2d0d0243ecbbed329e4674220e)
177e00bf9SAndreas Gohr<?php
209b1e97eSAndreas Gohr
309b1e97eSAndreas Gohruse dokuwiki\Extension\Plugin;
4c6d794b3SAndreas Gohruse dokuwiki\plugin\captcha\FileCookie;
509b1e97eSAndreas Gohruse dokuwiki\Utf8\PhpString;
609b1e97eSAndreas Gohr
777e00bf9SAndreas Gohr/**
877e00bf9SAndreas Gohr * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
977e00bf9SAndreas Gohr * @author     Andreas Gohr <andi@splitbrain.org>
1077e00bf9SAndreas Gohr */
117218f96cSAndreas Gohr
12a285df67SAndreas Gohr/**
13a285df67SAndreas Gohr * Class helper_plugin_captcha
14a285df67SAndreas Gohr */
1509b1e97eSAndreas Gohrclass helper_plugin_captcha extends Plugin
1618622736SAndreas Gohr{
1723d379a8SAndreas Gohr    protected $field_in = 'plugin__captcha';
1823d379a8SAndreas Gohr    protected $field_sec = 'plugin__captcha_secret';
1923d379a8SAndreas Gohr    protected $field_hp = 'plugin__captcha_honeypot';
2023d379a8SAndreas Gohr
2109b1e97eSAndreas Gohr    // region Public API
2209b1e97eSAndreas Gohr
2323d379a8SAndreas Gohr    /**
2423d379a8SAndreas Gohr     * Constructor. Initializes field names
2523d379a8SAndreas Gohr     */
2618622736SAndreas Gohr    public function __construct()
2718622736SAndreas Gohr    {
2809b1e97eSAndreas Gohr        $this->field_in = md5($this->fixedIdent() . $this->field_in);
2909b1e97eSAndreas Gohr        $this->field_sec = md5($this->fixedIdent() . $this->field_sec);
3009b1e97eSAndreas Gohr        $this->field_hp = md5($this->fixedIdent() . $this->field_hp);
3123d379a8SAndreas Gohr    }
3223d379a8SAndreas Gohr
3377e00bf9SAndreas Gohr    /**
3477e00bf9SAndreas Gohr     * Check if the CAPTCHA should be used. Always check this before using the methods below.
3577e00bf9SAndreas Gohr     *
3677e00bf9SAndreas Gohr     * @return bool true when the CAPTCHA should be used
3777e00bf9SAndreas Gohr     */
3818622736SAndreas Gohr    public function isEnabled()
3918622736SAndreas Gohr    {
4064382f29SAndreas Gohr        global $INPUT;
4164382f29SAndreas Gohr        if (!$this->getConf('forusers') && $INPUT->server->str('REMOTE_USER')) return false;
4277e00bf9SAndreas Gohr        return true;
4377e00bf9SAndreas Gohr    }
4477e00bf9SAndreas Gohr
4577e00bf9SAndreas Gohr    /**
4677e00bf9SAndreas Gohr     * Returns the HTML to display the CAPTCHA with the chosen method
4709b1e97eSAndreas Gohr     *
4809b1e97eSAndreas Gohr     * @return string The HTML to display the CAPTCHA
4977e00bf9SAndreas Gohr     */
5018622736SAndreas Gohr    public function getHTML()
5118622736SAndreas Gohr    {
5277e00bf9SAndreas Gohr        global $ID;
5377e00bf9SAndreas Gohr
5409b1e97eSAndreas Gohr        $rand = (float)(random_int(0, 10000)) / 10000;
55c6d794b3SAndreas Gohr        $cookie = new FileCookie($this->fixedIdent(), $rand);
56c6d794b3SAndreas Gohr        $cookie->set();
57a285df67SAndreas Gohr
589e312724SAndreas Gohr        if ($this->getConf('mode') == 'math') {
5909b1e97eSAndreas Gohr            $code = $this->generateMath($this->fixedIdent(), $rand);
609e312724SAndreas Gohr            $code = $code[0];
619e312724SAndreas Gohr            $text = $this->getLang('fillmath');
62df8afac4SAndreas Gohr        } elseif ($this->getConf('mode') == 'question') {
63a285df67SAndreas Gohr            $code = ''; // not used
64df8afac4SAndreas Gohr            $text = $this->getConf('question');
659e312724SAndreas Gohr        } else {
6609b1e97eSAndreas Gohr            $code = $this->generateCaptchaCode($this->fixedIdent(), $rand);
679e312724SAndreas Gohr            $text = $this->getLang('fillcaptcha');
689e312724SAndreas Gohr        }
69f044313dSAndreas Gohr        $secret = $this->encrypt($rand);
7077e00bf9SAndreas Gohr
7128c14643SAndreas Gohr        $txtlen = $this->getConf('lettercount');
7228c14643SAndreas Gohr
7377e00bf9SAndreas Gohr        $out = '';
7477e00bf9SAndreas Gohr        $out .= '<div id="plugin__captcha_wrapper">';
7523d379a8SAndreas Gohr        $out .= '<input type="hidden" name="' . $this->field_sec . '" value="' . hsc($secret) . '" />';
769e312724SAndreas Gohr        $out .= '<label for="plugin__captcha">' . $text . '</label> ';
7723d379a8SAndreas Gohr
7877e00bf9SAndreas Gohr        switch ($this->getConf('mode')) {
799e312724SAndreas Gohr            case 'math':
809d6f09afSAndreas Gohr            case 'text':
8109b1e97eSAndreas Gohr                $out .= $this->obfuscateText($code);
8277e00bf9SAndreas Gohr                break;
8377e00bf9SAndreas Gohr            case 'js':
841cd9cde7SAndreas Gohr                $out .= sprintf('<span id="plugin__captcha_code">%s</span>', $this->obfuscateText($code));
8577e00bf9SAndreas Gohr                break;
8608f248e4SAndreas Gohr            case 'svg':
871cd9cde7SAndreas Gohr                $out .= $this->htmlSvg($code);
8808f248e4SAndreas Gohr                break;
8908f248e4SAndreas Gohr            case 'svgaudio':
901cd9cde7SAndreas Gohr                $out .= $this->htmlSvg($code);
911cd9cde7SAndreas Gohr                $out .= $this->htmlAudioLink($secret, $ID);
9208f248e4SAndreas Gohr                break;
9377e00bf9SAndreas Gohr            case 'image':
941cd9cde7SAndreas Gohr                $out .= $this->htmlImage($ID, $secret);
9577e00bf9SAndreas Gohr                break;
9677e00bf9SAndreas Gohr            case 'audio':
971cd9cde7SAndreas Gohr                $out .= $this->htmlImage($ID, $secret);
981cd9cde7SAndreas Gohr                $out .= $this->htmlAudioLink($secret, $ID);
9977e00bf9SAndreas Gohr                break;
10052e95008SAndreas Gohr            case 'figlet':
1011cd9cde7SAndreas Gohr                $out .= $this->htmlFiglet($code);
10252e95008SAndreas Gohr                break;
10377e00bf9SAndreas Gohr        }
104df8afac4SAndreas Gohr        $out .= ' <input type="text" size="' . $txtlen . '" name="' . $this->field_in . '" class="edit" /> ';
10523d379a8SAndreas Gohr
10623d379a8SAndreas Gohr        // add honeypot field
1071cd9cde7SAndreas Gohr        $out .= sprintf(
1081cd9cde7SAndreas Gohr            '<label class="no">%s<input type="text" name="%s" /></label>',
1091cd9cde7SAndreas Gohr            $this->getLang('honeypot'),
1101cd9cde7SAndreas Gohr            $this->field_hp
1111cd9cde7SAndreas Gohr        );
11277e00bf9SAndreas Gohr        $out .= '</div>';
11377e00bf9SAndreas Gohr        return $out;
11477e00bf9SAndreas Gohr    }
11577e00bf9SAndreas Gohr
11677e00bf9SAndreas Gohr    /**
11718622736SAndreas Gohr     * Checks if the CAPTCHA was solved correctly
11877e00bf9SAndreas Gohr     *
11977e00bf9SAndreas Gohr     * @param bool $msg when true, an error will be signalled through the msg() method
12077e00bf9SAndreas Gohr     * @return bool true when the answer was correct, otherwise false
12177e00bf9SAndreas Gohr     */
12218622736SAndreas Gohr    public function check($msg = true)
12318622736SAndreas Gohr    {
124478e363cSAndreas Gohr        global $INPUT;
125478e363cSAndreas Gohr
126478e363cSAndreas Gohr        $field_sec = $INPUT->str($this->field_sec);
127478e363cSAndreas Gohr        $field_in = $INPUT->str($this->field_in);
128478e363cSAndreas Gohr        $field_hp = $INPUT->str($this->field_hp);
129478e363cSAndreas Gohr
130478e363cSAndreas Gohr        // reconstruct captcha from provided $field_sec
131478e363cSAndreas Gohr        $rand = $this->decrypt($field_sec);
1329e312724SAndreas Gohr
1339e312724SAndreas Gohr        if ($this->getConf('mode') == 'math') {
13409b1e97eSAndreas Gohr            $code = $this->generateMath($this->fixedIdent(), $rand);
1359e312724SAndreas Gohr            $code = $code[1];
136df8afac4SAndreas Gohr        } elseif ($this->getConf('mode') == 'question') {
137df8afac4SAndreas Gohr            $code = $this->getConf('answer');
1389e312724SAndreas Gohr        } else {
13909b1e97eSAndreas Gohr            $code = $this->generateCaptchaCode($this->fixedIdent(), $rand);
1409e312724SAndreas Gohr        }
14177e00bf9SAndreas Gohr
142478e363cSAndreas Gohr        // compare values
14309b1e97eSAndreas Gohr        if (
14409b1e97eSAndreas Gohr            !$field_sec ||
145478e363cSAndreas Gohr            !$field_in ||
14614e271ebSPatrick Brown            $rand === false ||
14709b1e97eSAndreas Gohr            PhpString::strtolower($field_in) != PhpString::strtolower($code) ||
148a285df67SAndreas Gohr            trim($field_hp) !== '' ||
149c6d794b3SAndreas Gohr            !(new FileCookie($this->fixedIdent(), $rand))->check()
15023d379a8SAndreas Gohr        ) {
15177e00bf9SAndreas Gohr            if ($msg) msg($this->getLang('testfailed'), -1);
15277e00bf9SAndreas Gohr            return false;
15377e00bf9SAndreas Gohr        }
15477e00bf9SAndreas Gohr        return true;
15577e00bf9SAndreas Gohr    }
15677e00bf9SAndreas Gohr
15709b1e97eSAndreas Gohr    // endregion
15809b1e97eSAndreas Gohr
15909b1e97eSAndreas Gohr    // region Captcha Generation methods
16009b1e97eSAndreas Gohr
161a285df67SAndreas Gohr    /**
16277e00bf9SAndreas Gohr     * Build a semi-secret fixed string identifying the current page and user
16377e00bf9SAndreas Gohr     *
16477e00bf9SAndreas Gohr     * This string is always the same for the current user when editing the same
16527d84d8dSAndreas Gohr     * page revision, but only for one day. Editing a page before midnight and saving
16627d84d8dSAndreas Gohr     * after midnight will result in a failed CAPTCHA once, but makes sure it can
16727d84d8dSAndreas Gohr     * not be reused which is especially important for the registration form where the
16827d84d8dSAndreas Gohr     * $ID usually won't change.
169104ec268SAndreas Gohr     *
170104ec268SAndreas Gohr     * @return string
17177e00bf9SAndreas Gohr     */
17209b1e97eSAndreas Gohr    public function fixedIdent()
17318622736SAndreas Gohr    {
17477e00bf9SAndreas Gohr        global $ID;
17577e00bf9SAndreas Gohr        $lm = @filemtime(wikiFN($ID));
17627d84d8dSAndreas Gohr        $td = date('Y-m-d');
17763609b6eSAndreas Gohr        $ip = clientIP();
17863609b6eSAndreas Gohr        $salt = auth_cookiesalt();
17963609b6eSAndreas Gohr
18009b1e97eSAndreas Gohr        return sha1(implode("\n", [$ID, $lm, $td, $ip, $salt]));
18177e00bf9SAndreas Gohr    }
18277e00bf9SAndreas Gohr
18377e00bf9SAndreas Gohr    /**
18409b1e97eSAndreas Gohr     * Generate a magic code based on the given data
1859d6f09afSAndreas Gohr     *
18609b1e97eSAndreas Gohr     * This "magic" code represents the given fixed identifier (see fixedIdent()) and the given
18709b1e97eSAndreas Gohr     * random number. It is used to generate the actual CAPTCHA code.
1889d6f09afSAndreas Gohr     *
18909b1e97eSAndreas Gohr     * @param $ident string the fixed part, any string
190a02b2219SPatrick Brown     * @param $rand  float  some random number between 0 and 1
191a02b2219SPatrick Brown     * @return string
192a02b2219SPatrick Brown     */
19309b1e97eSAndreas Gohr    protected function generateMagicCode($ident, $rand)
19418622736SAndreas Gohr    {
19509b1e97eSAndreas Gohr        $ident = hexdec(substr(md5($ident), 5, 5)); // use part of the md5 to generate an int
19609b1e97eSAndreas Gohr        $rand *= 0xFFFFF; // bitmask from the random number
1975697ecf8SAndreas Gohr        $comb = (int)$rand ^ $ident; // combine both values
1985697ecf8SAndreas Gohr        return md5($comb);
199a02b2219SPatrick Brown    }
200a02b2219SPatrick Brown
201a02b2219SPatrick Brown    /**
20209b1e97eSAndreas Gohr     * Generates a char string based on the given data
20377e00bf9SAndreas Gohr     *
20409b1e97eSAndreas Gohr     * The string is pseudo random based on a fixed identifier (see fixedIdent()) and a random number.
20509b1e97eSAndreas Gohr     *
20609b1e97eSAndreas Gohr     * @param $ident string the fixed part, any string
207104ec268SAndreas Gohr     * @param $rand  float  some random number between 0 and 1
20823d379a8SAndreas Gohr     * @return string
20977e00bf9SAndreas Gohr     */
21009b1e97eSAndreas Gohr    public function generateCaptchaCode($ident, $rand)
21118622736SAndreas Gohr    {
21209b1e97eSAndreas Gohr        $numbers = $this->generateMagicCode($ident, $rand);
21377e00bf9SAndreas Gohr
21477e00bf9SAndreas Gohr        // now create the letters
21577e00bf9SAndreas Gohr        $code = '';
2169a516edaSPatrick Brown        $lettercount = $this->getConf('lettercount') * 2;
2179a516edaSPatrick Brown        if ($lettercount > strlen($numbers)) $lettercount = strlen($numbers);
2189a516edaSPatrick Brown        for ($i = 0; $i < $lettercount; $i += 2) {
21977e00bf9SAndreas Gohr            $code .= chr(floor(hexdec($numbers[$i] . $numbers[$i + 1]) / 10) + 65);
22077e00bf9SAndreas Gohr        }
22177e00bf9SAndreas Gohr
22277e00bf9SAndreas Gohr        return $code;
22377e00bf9SAndreas Gohr    }
22477e00bf9SAndreas Gohr
2259e312724SAndreas Gohr    /**
2269e312724SAndreas Gohr     * Create a mathematical task and its result
2279e312724SAndreas Gohr     *
22809b1e97eSAndreas Gohr     * @param $ident string the fixed part, any string
229104ec268SAndreas Gohr     * @param $rand  float  some random number between 0 and 1
23009b1e97eSAndreas Gohr     * @return array [task, result]
2319e312724SAndreas Gohr     */
23209b1e97eSAndreas Gohr    protected function generateMath($ident, $rand)
23318622736SAndreas Gohr    {
23409b1e97eSAndreas Gohr        $numbers = $this->generateMagicCode($ident, $rand);
2359e312724SAndreas Gohr
2369e312724SAndreas Gohr        // first letter is the operator (+/-)
2379e312724SAndreas Gohr        $op = (hexdec($numbers[0]) > 8) ? -1 : 1;
23809b1e97eSAndreas Gohr        $num = [hexdec($numbers[1] . $numbers[2]), hexdec($numbers[3])];
2399e312724SAndreas Gohr
2409e312724SAndreas Gohr        // we only want positive results
2419e312724SAndreas Gohr        if (($op < 0) && ($num[0] < $num[1])) rsort($num);
2429e312724SAndreas Gohr
2439e312724SAndreas Gohr        // prepare result and task text
2449e312724SAndreas Gohr        $res = $num[0] + ($num[1] * $op);
2459bc1fab2SApostolos P. Tsompanopoulos        $task = $num[0] . (($op < 0) ? '-' : '+') . $num[1] . '= ';
2469e312724SAndreas Gohr
24709b1e97eSAndreas Gohr        return [$task, $res];
2489e312724SAndreas Gohr    }
24977e00bf9SAndreas Gohr
25009b1e97eSAndreas Gohr    // endregion
25109b1e97eSAndreas Gohr
25209b1e97eSAndreas Gohr    // region Output Builders
25309b1e97eSAndreas Gohr
25477e00bf9SAndreas Gohr    /**
25577e00bf9SAndreas Gohr     * Create a CAPTCHA image
256104ec268SAndreas Gohr     *
257104ec268SAndreas Gohr     * @param string $text the letters to display
25809b1e97eSAndreas Gohr     * @return string The image data
25977e00bf9SAndreas Gohr     */
26009b1e97eSAndreas Gohr    public function imageCaptcha($text)
26118622736SAndreas Gohr    {
26277e00bf9SAndreas Gohr        $w = $this->getConf('width');
26377e00bf9SAndreas Gohr        $h = $this->getConf('height');
26477e00bf9SAndreas Gohr
26509b1e97eSAndreas Gohr        $fonts = glob(__DIR__ . '/fonts/*.ttf');
266dc091fd0SAndreas Gohr
26777e00bf9SAndreas Gohr        // create a white image
26828c14643SAndreas Gohr        $img = imagecreatetruecolor($w, $h);
26928c14643SAndreas Gohr        $white = imagecolorallocate($img, 255, 255, 255);
27028c14643SAndreas Gohr        imagefill($img, 0, 0, $white);
27177e00bf9SAndreas Gohr
27277e00bf9SAndreas Gohr        // add some lines as background noise
27377e00bf9SAndreas Gohr        for ($i = 0; $i < 30; $i++) {
27409b1e97eSAndreas Gohr            $color = imagecolorallocate($img, random_int(100, 250), random_int(100, 250), random_int(100, 250));
27509b1e97eSAndreas Gohr            imageline($img, random_int(0, $w), random_int(0, $h), random_int(0, $w), random_int(0, $h), $color);
27677e00bf9SAndreas Gohr        }
27777e00bf9SAndreas Gohr
27877e00bf9SAndreas Gohr        // draw the letters
27928c14643SAndreas Gohr        $txtlen = strlen($text);
28028c14643SAndreas Gohr        for ($i = 0; $i < $txtlen; $i++) {
281dc091fd0SAndreas Gohr            $font = $fonts[array_rand($fonts)];
28209b1e97eSAndreas Gohr            $color = imagecolorallocate($img, random_int(0, 100), random_int(0, 100), random_int(0, 100));
28309b1e97eSAndreas Gohr            $size = random_int(floor($h / 1.8), floor($h * 0.7));
28409b1e97eSAndreas Gohr            $angle = random_int(-35, 35);
28577e00bf9SAndreas Gohr
28628c14643SAndreas Gohr            $x = ($w * 0.05) + $i * floor($w * 0.9 / $txtlen);
28777e00bf9SAndreas Gohr            $cheight = $size + ($size * 0.5);
28877e00bf9SAndreas Gohr            $y = floor($h / 2 + $cheight / 3.8);
28977e00bf9SAndreas Gohr
29077e00bf9SAndreas Gohr            imagettftext($img, $size, $angle, $x, $y, $color, $font, $text[$i]);
29177e00bf9SAndreas Gohr        }
29277e00bf9SAndreas Gohr
29309b1e97eSAndreas Gohr        ob_start();
29477e00bf9SAndreas Gohr        imagepng($img);
29509b1e97eSAndreas Gohr        $image = ob_get_clean();
29677e00bf9SAndreas Gohr        imagedestroy($img);
29709b1e97eSAndreas Gohr        return $image;
29808f248e4SAndreas Gohr    }
29908f248e4SAndreas Gohr
30008f248e4SAndreas Gohr    /**
30163609b6eSAndreas Gohr     * Generate an audio captcha
30263609b6eSAndreas Gohr     *
30363609b6eSAndreas Gohr     * @param string $text
30409b1e97eSAndreas Gohr     * @return string The joined wav files
30563609b6eSAndreas Gohr     */
30609b1e97eSAndreas Gohr    public function audioCaptcha($text)
30709b1e97eSAndreas Gohr    {
30863609b6eSAndreas Gohr        global $conf;
30963609b6eSAndreas Gohr
31063609b6eSAndreas Gohr        $lc = __DIR__ . '/lang/' . $conf['lang'] . '/audio/';
31163609b6eSAndreas Gohr        $en = __DIR__ . '/lang/en/audio/';
31263609b6eSAndreas Gohr
31363609b6eSAndreas Gohr        $wavs = [];
31463609b6eSAndreas Gohr
31563609b6eSAndreas Gohr        $text = strtolower($text);
31663609b6eSAndreas Gohr        $txtlen = strlen($text);
31763609b6eSAndreas Gohr        for ($i = 0; $i < $txtlen; $i++) {
31863609b6eSAndreas Gohr            $char = $text[$i];
31963609b6eSAndreas Gohr            $file = $lc . $char . '.wav';
32063609b6eSAndreas Gohr            if (!@file_exists($file)) $file = $en . $char . '.wav';
32163609b6eSAndreas Gohr            $wavs[] = $file;
32263609b6eSAndreas Gohr        }
32363609b6eSAndreas Gohr
32409b1e97eSAndreas Gohr        return $this->joinwavs($wavs);
32563609b6eSAndreas Gohr    }
32663609b6eSAndreas Gohr
32763609b6eSAndreas Gohr    /**
32809b1e97eSAndreas Gohr     * Create an SVG of the given text
32909b1e97eSAndreas Gohr     *
33009b1e97eSAndreas Gohr     * @param string $text
33109b1e97eSAndreas Gohr     * @return string
33209b1e97eSAndreas Gohr     */
33309b1e97eSAndreas Gohr    public function svgCaptcha($text)
33409b1e97eSAndreas Gohr    {
33509b1e97eSAndreas Gohr        require_once(__DIR__ . '/EasySVG.php');
33609b1e97eSAndreas Gohr
33709b1e97eSAndreas Gohr        $fonts = glob(__DIR__ . '/fonts/*.svg');
33809b1e97eSAndreas Gohr
33909b1e97eSAndreas Gohr        $x = 0; // where we start to draw
34009b1e97eSAndreas Gohr        $y = 100; // our max height
34109b1e97eSAndreas Gohr
34209b1e97eSAndreas Gohr        $svg = new EasySVG();
34309b1e97eSAndreas Gohr
34409b1e97eSAndreas Gohr        // draw the letters
34509b1e97eSAndreas Gohr        $txtlen = strlen($text);
34609b1e97eSAndreas Gohr        for ($i = 0; $i < $txtlen; $i++) {
34709b1e97eSAndreas Gohr            $char = $text[$i];
34809b1e97eSAndreas Gohr            $size = random_int($y / 2, $y - $y * 0.1); // 50-90%
34909b1e97eSAndreas Gohr            $svg->setFontSVG($fonts[array_rand($fonts)]);
35009b1e97eSAndreas Gohr
35109b1e97eSAndreas Gohr            $svg->setFontSize($size);
35209b1e97eSAndreas Gohr            $svg->setLetterSpacing(round(random_int(1, 4) / 10, 2)); // 0.1 - 0.4
35309b1e97eSAndreas Gohr            $svg->addText($char, $x, random_int(0, round($y - $size))); // random up and down
35409b1e97eSAndreas Gohr
35509b1e97eSAndreas Gohr            [$w] = $svg->textDimensions($char);
35609b1e97eSAndreas Gohr            $x += $w;
35709b1e97eSAndreas Gohr        }
35809b1e97eSAndreas Gohr
35909b1e97eSAndreas Gohr        $svg->addAttribute('width', $x . 'px');
36009b1e97eSAndreas Gohr        $svg->addAttribute('height', $y . 'px');
36109b1e97eSAndreas Gohr        $svg->addAttribute('viewbox', "0 0 $x $y");
36209b1e97eSAndreas Gohr        return $svg->asXML();
36309b1e97eSAndreas Gohr    }
36409b1e97eSAndreas Gohr
3651cd9cde7SAndreas Gohr    /**
3661cd9cde7SAndreas Gohr     * Inline SVG showing the given code
3671cd9cde7SAndreas Gohr     *
3681cd9cde7SAndreas Gohr     * @param string $code
3691cd9cde7SAndreas Gohr     * @return string
3701cd9cde7SAndreas Gohr     */
3711cd9cde7SAndreas Gohr    protected function htmlSvg($code)
3721cd9cde7SAndreas Gohr    {
3731cd9cde7SAndreas Gohr        return sprintf(
3741cd9cde7SAndreas Gohr            '<span class="svg" style="width:%spx; height:%spx">%s</span>',
3751cd9cde7SAndreas Gohr            $this->getConf('width'),
3761cd9cde7SAndreas Gohr            $this->getConf('height'),
3771cd9cde7SAndreas Gohr            $this->svgCaptcha($code)
3781cd9cde7SAndreas Gohr        );
3791cd9cde7SAndreas Gohr    }
3801cd9cde7SAndreas Gohr
3811cd9cde7SAndreas Gohr    /**
3821cd9cde7SAndreas Gohr     * HTML for an img tag for the image captcha
3831cd9cde7SAndreas Gohr     *
3841cd9cde7SAndreas Gohr     * @param string $ID the page ID this is displayed on
3851cd9cde7SAndreas Gohr     * @param string $secret the encrypted random number
3861cd9cde7SAndreas Gohr     * @return string
3871cd9cde7SAndreas Gohr     */
3881cd9cde7SAndreas Gohr    protected function htmlImage($ID, $secret)
3891cd9cde7SAndreas Gohr    {
3901cd9cde7SAndreas Gohr        $img = DOKU_BASE . 'lib/plugins/captcha/img.php';
3911cd9cde7SAndreas Gohr        $param = buildURLparams([
3921cd9cde7SAndreas Gohr            'secret' => $secret,
3931cd9cde7SAndreas Gohr            'id' => $ID,
3941cd9cde7SAndreas Gohr        ]);
3951cd9cde7SAndreas Gohr
3961cd9cde7SAndreas Gohr        return sprintf(
3971cd9cde7SAndreas Gohr            '<img src="%s?%s" width="%d" height="%d" alt="" />',
3981cd9cde7SAndreas Gohr            $img,
3991cd9cde7SAndreas Gohr            $param,
4001cd9cde7SAndreas Gohr            $this->getConf('width'),
4011cd9cde7SAndreas Gohr            $this->getConf('height')
4021cd9cde7SAndreas Gohr        );
4031cd9cde7SAndreas Gohr    }
4041cd9cde7SAndreas Gohr
4051cd9cde7SAndreas Gohr    /**
4061cd9cde7SAndreas Gohr     * HTML for a link to the audio captcha
4071cd9cde7SAndreas Gohr     *
4081cd9cde7SAndreas Gohr     * @param string $secret the encrypted random number
4091cd9cde7SAndreas Gohr     * @param string $ID the page ID this is displayed on
4101cd9cde7SAndreas Gohr     * @return string
4111cd9cde7SAndreas Gohr     */
4121cd9cde7SAndreas Gohr    protected function htmlAudioLink($secret, $ID)
4131cd9cde7SAndreas Gohr    {
414*184a2060SAndreas Gohr
4151cd9cde7SAndreas Gohr        $url = DOKU_BASE . 'lib/plugins/captcha/wav.php';
4161cd9cde7SAndreas Gohr        $param = buildURLparams([
4171cd9cde7SAndreas Gohr            'secret' => $secret,
4181cd9cde7SAndreas Gohr            'id' => $ID,
4191cd9cde7SAndreas Gohr        ]);
4201cd9cde7SAndreas Gohr
421*184a2060SAndreas Gohr        $icon = inlineSVG(__DIR__ . '/ear-hearing.svg');
4221cd9cde7SAndreas Gohr
4231cd9cde7SAndreas Gohr        return sprintf(
424*184a2060SAndreas Gohr            '<a href="%s?%s" class="JSnocheck audiolink" title="%s" style="height: %spx">%s</a>',
4251cd9cde7SAndreas Gohr            $url,
4261cd9cde7SAndreas Gohr            $param,
4271cd9cde7SAndreas Gohr            $this->getLang('soundlink'),
428*184a2060SAndreas Gohr            $this->getConf('height'),
4291cd9cde7SAndreas Gohr            $icon
4301cd9cde7SAndreas Gohr        );
4311cd9cde7SAndreas Gohr    }
4321cd9cde7SAndreas Gohr
4331cd9cde7SAndreas Gohr    /**
4341cd9cde7SAndreas Gohr     * The HTML to show a figlet captcha
4351cd9cde7SAndreas Gohr     *
4361cd9cde7SAndreas Gohr     * @param string $code the code to display
4371cd9cde7SAndreas Gohr     * @return string
4381cd9cde7SAndreas Gohr     */
4391cd9cde7SAndreas Gohr    protected function htmlFiglet($code)
4401cd9cde7SAndreas Gohr    {
4411cd9cde7SAndreas Gohr        require_once(__DIR__ . '/figlet.php');
4421cd9cde7SAndreas Gohr        $figlet = new phpFiglet();
4431cd9cde7SAndreas Gohr        if ($figlet->loadfont(__DIR__ . '/figlet.flf')) {
4441cd9cde7SAndreas Gohr            return '<pre>' . rtrim($figlet->fetch($code)) . '</pre>';
4451cd9cde7SAndreas Gohr        } else {
4461cd9cde7SAndreas Gohr            msg('Failed to load figlet.flf font file. CAPTCHA broken', -1);
4471cd9cde7SAndreas Gohr        }
4481cd9cde7SAndreas Gohr        return 'FAIL';
4491cd9cde7SAndreas Gohr    }
4501cd9cde7SAndreas Gohr
45109b1e97eSAndreas Gohr    // endregion
45209b1e97eSAndreas Gohr
45309b1e97eSAndreas Gohr    // region Utilities
45409b1e97eSAndreas Gohr
45509b1e97eSAndreas Gohr    /**
456f044313dSAndreas Gohr     * Encrypt the given string with the cookie salt
457f044313dSAndreas Gohr     *
458f044313dSAndreas Gohr     * @param string $data
459f044313dSAndreas Gohr     * @return string
460f044313dSAndreas Gohr     */
46118622736SAndreas Gohr    public function encrypt($data)
46218622736SAndreas Gohr    {
46309b1e97eSAndreas Gohr        $data = auth_encrypt($data, auth_cookiesalt());
464f044313dSAndreas Gohr        return base64_encode($data);
465f044313dSAndreas Gohr    }
466f044313dSAndreas Gohr
467f044313dSAndreas Gohr    /**
468f044313dSAndreas Gohr     * Decrypt the given string with the cookie salt
469f044313dSAndreas Gohr     *
470f044313dSAndreas Gohr     * @param string $data
471f044313dSAndreas Gohr     * @return string
472f044313dSAndreas Gohr     */
47318622736SAndreas Gohr    public function decrypt($data)
47418622736SAndreas Gohr    {
475f044313dSAndreas Gohr        $data = base64_decode($data);
47609870f99SPatrick Brown        if ($data === false || $data === '') return false;
477f044313dSAndreas Gohr
47809b1e97eSAndreas Gohr        return auth_decrypt($data, auth_cookiesalt());
479f044313dSAndreas Gohr    }
48009b1e97eSAndreas Gohr
48109b1e97eSAndreas Gohr    /**
48209b1e97eSAndreas Gohr     * Adds random space characters within the given text
48309b1e97eSAndreas Gohr     *
48409b1e97eSAndreas Gohr     * Keeps subsequent numbers without spaces (for math problem)
48509b1e97eSAndreas Gohr     *
48609b1e97eSAndreas Gohr     * @param $text
48709b1e97eSAndreas Gohr     * @return string
48809b1e97eSAndreas Gohr     */
48909b1e97eSAndreas Gohr    protected function obfuscateText($text)
49009b1e97eSAndreas Gohr    {
49109b1e97eSAndreas Gohr        $new = '';
49209b1e97eSAndreas Gohr
49309b1e97eSAndreas Gohr        $spaces = [
49409b1e97eSAndreas Gohr            "\r",
49509b1e97eSAndreas Gohr            "\n",
49609b1e97eSAndreas Gohr            "\r\n",
49709b1e97eSAndreas Gohr            ' ',
49809b1e97eSAndreas Gohr            "\xC2\xA0",
49909b1e97eSAndreas Gohr            // \u00A0    NO-BREAK SPACE
50009b1e97eSAndreas Gohr            "\xE2\x80\x80",
50109b1e97eSAndreas Gohr            // \u2000    EN QUAD
50209b1e97eSAndreas Gohr            "\xE2\x80\x81",
50309b1e97eSAndreas Gohr            // \u2001    EM QUAD
50409b1e97eSAndreas Gohr            "\xE2\x80\x82",
50509b1e97eSAndreas Gohr            // \u2002    EN SPACE
50609b1e97eSAndreas Gohr            //         "\xE2\x80\x83", // \u2003    EM SPACE
50709b1e97eSAndreas Gohr            "\xE2\x80\x84",
50809b1e97eSAndreas Gohr            // \u2004    THREE-PER-EM SPACE
50909b1e97eSAndreas Gohr            "\xE2\x80\x85",
51009b1e97eSAndreas Gohr            // \u2005    FOUR-PER-EM SPACE
51109b1e97eSAndreas Gohr            "\xE2\x80\x86",
51209b1e97eSAndreas Gohr            // \u2006    SIX-PER-EM SPACE
51309b1e97eSAndreas Gohr            "\xE2\x80\x87",
51409b1e97eSAndreas Gohr            // \u2007    FIGURE SPACE
51509b1e97eSAndreas Gohr            "\xE2\x80\x88",
51609b1e97eSAndreas Gohr            // \u2008    PUNCTUATION SPACE
51709b1e97eSAndreas Gohr            "\xE2\x80\x89",
51809b1e97eSAndreas Gohr            // \u2009    THIN SPACE
51909b1e97eSAndreas Gohr            "\xE2\x80\x8A",
52009b1e97eSAndreas Gohr            // \u200A    HAIR SPACE
52109b1e97eSAndreas Gohr            "\xE2\x80\xAF",
52209b1e97eSAndreas Gohr            // \u202F    NARROW NO-BREAK SPACE
52309b1e97eSAndreas Gohr            "\xE2\x81\x9F",
52409b1e97eSAndreas Gohr            // \u205F    MEDIUM MATHEMATICAL SPACE
52509b1e97eSAndreas Gohr            "\xE1\xA0\x8E\r\n",
52609b1e97eSAndreas Gohr            // \u180E    MONGOLIAN VOWEL SEPARATOR
52709b1e97eSAndreas Gohr            "\xE2\x80\x8B\r\n",
52809b1e97eSAndreas Gohr            // \u200B    ZERO WIDTH SPACE
52909b1e97eSAndreas Gohr            "\xEF\xBB\xBF\r\n",
53009b1e97eSAndreas Gohr        ];
53109b1e97eSAndreas Gohr
53209b1e97eSAndreas Gohr        $len = strlen($text);
53309b1e97eSAndreas Gohr        for ($i = 0; $i < $len - 1; $i++) {
53409b1e97eSAndreas Gohr            $new .= $text[$i];
53509b1e97eSAndreas Gohr
53609b1e97eSAndreas Gohr            if (!is_numeric($text[$i + 1])) {
53709b1e97eSAndreas Gohr                $new .= $spaces[array_rand($spaces)];
53809b1e97eSAndreas Gohr            }
53909b1e97eSAndreas Gohr        }
54009b1e97eSAndreas Gohr        $new .= $text[$len - 1];
54109b1e97eSAndreas Gohr        return $new;
542f044313dSAndreas Gohr    }
543969b14c4SAndreas Gohr
54463609b6eSAndreas Gohr
54563609b6eSAndreas Gohr    /**
54663609b6eSAndreas Gohr     * Join multiple wav files
54763609b6eSAndreas Gohr     *
54863609b6eSAndreas Gohr     * All wave files need to have the same format and need to be uncompressed.
54963609b6eSAndreas Gohr     * The headers of the last file will be used (with recalculated datasize
55063609b6eSAndreas Gohr     * of course)
55163609b6eSAndreas Gohr     *
55263609b6eSAndreas Gohr     * @link http://ccrma.stanford.edu/CCRMA/Courses/422/projects/WaveFormat/
55363609b6eSAndreas Gohr     * @link http://www.thescripts.com/forum/thread3770.html
55463609b6eSAndreas Gohr     */
55563609b6eSAndreas Gohr    protected function joinwavs($wavs)
55663609b6eSAndreas Gohr    {
55709b1e97eSAndreas Gohr        $fields = implode(
55809b1e97eSAndreas Gohr            '/',
55909b1e97eSAndreas Gohr            [
56063609b6eSAndreas Gohr                'H8ChunkID',
56163609b6eSAndreas Gohr                'VChunkSize',
56263609b6eSAndreas Gohr                'H8Format',
56363609b6eSAndreas Gohr                'H8Subchunk1ID',
56463609b6eSAndreas Gohr                'VSubchunk1Size',
56563609b6eSAndreas Gohr                'vAudioFormat',
56663609b6eSAndreas Gohr                'vNumChannels',
56763609b6eSAndreas Gohr                'VSampleRate',
56863609b6eSAndreas Gohr                'VByteRate',
56963609b6eSAndreas Gohr                'vBlockAlign',
57009b1e97eSAndreas Gohr                'vBitsPerSample'
57109b1e97eSAndreas Gohr            ]
57263609b6eSAndreas Gohr        );
57363609b6eSAndreas Gohr
57463609b6eSAndreas Gohr        $data = '';
57563609b6eSAndreas Gohr        foreach ($wavs as $wav) {
57663609b6eSAndreas Gohr            $fp = fopen($wav, 'rb');
57763609b6eSAndreas Gohr            $header = fread($fp, 36);
57863609b6eSAndreas Gohr            $info = unpack($fields, $header);
57963609b6eSAndreas Gohr
58063609b6eSAndreas Gohr            // read optional extra stuff
58163609b6eSAndreas Gohr            if ($info['Subchunk1Size'] > 16) {
58263609b6eSAndreas Gohr                $header .= fread($fp, ($info['Subchunk1Size'] - 16));
58363609b6eSAndreas Gohr            }
58463609b6eSAndreas Gohr
58563609b6eSAndreas Gohr            // read SubChunk2ID
58663609b6eSAndreas Gohr            $header .= fread($fp, 4);
58763609b6eSAndreas Gohr
58863609b6eSAndreas Gohr            // read Subchunk2Size
58963609b6eSAndreas Gohr            $size = unpack('vsize', fread($fp, 4));
59063609b6eSAndreas Gohr            $size = $size['size'];
59163609b6eSAndreas Gohr
59263609b6eSAndreas Gohr            // read data
59363609b6eSAndreas Gohr            $data .= fread($fp, $size);
59463609b6eSAndreas Gohr        }
59563609b6eSAndreas Gohr
59663609b6eSAndreas Gohr        return $header . pack('V', strlen($data)) . $data;
59763609b6eSAndreas Gohr    }
59863609b6eSAndreas Gohr
59909b1e97eSAndreas Gohr    // endregion
60077e00bf9SAndreas Gohr}
601