xref: /plugin/captcha/helper.php (revision 1cd9cde7fa3af3699251c409d44d1efc6f383e17)
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':
84*1cd9cde7SAndreas Gohr                $out .= sprintf('<span id="plugin__captcha_code">%s</span>', $this->obfuscateText($code));
8577e00bf9SAndreas Gohr                break;
8608f248e4SAndreas Gohr            case 'svg':
87*1cd9cde7SAndreas Gohr                $out .= $this->htmlSvg($code);
8808f248e4SAndreas Gohr                break;
8908f248e4SAndreas Gohr            case 'svgaudio':
90*1cd9cde7SAndreas Gohr                $out .= $this->htmlSvg($code);
91*1cd9cde7SAndreas Gohr                $out .= $this->htmlAudioLink($secret, $ID);
9208f248e4SAndreas Gohr                break;
9377e00bf9SAndreas Gohr            case 'image':
94*1cd9cde7SAndreas Gohr                $out .= $this->htmlImage($ID, $secret);
9577e00bf9SAndreas Gohr                break;
9677e00bf9SAndreas Gohr            case 'audio':
97*1cd9cde7SAndreas Gohr                $out .= $this->htmlImage($ID, $secret);
98*1cd9cde7SAndreas Gohr                $out .= $this->htmlAudioLink($secret, $ID);
9977e00bf9SAndreas Gohr                break;
10052e95008SAndreas Gohr            case 'figlet':
101*1cd9cde7SAndreas 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
107*1cd9cde7SAndreas Gohr        $out .= sprintf(
108*1cd9cde7SAndreas Gohr            '<label class="no">%s<input type="text" name="%s" /></label>',
109*1cd9cde7SAndreas Gohr            $this->getLang('honeypot'),
110*1cd9cde7SAndreas Gohr            $this->field_hp
111*1cd9cde7SAndreas 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
19709b1e97eSAndreas Gohr        return md5($rand ^ $ident); // combine both values
198a02b2219SPatrick Brown    }
199a02b2219SPatrick Brown
200a02b2219SPatrick Brown    /**
20109b1e97eSAndreas Gohr     * Generates a char string based on the given data
20277e00bf9SAndreas Gohr     *
20309b1e97eSAndreas Gohr     * The string is pseudo random based on a fixed identifier (see fixedIdent()) and a random number.
20409b1e97eSAndreas Gohr     *
20509b1e97eSAndreas Gohr     * @param $ident string the fixed part, any string
206104ec268SAndreas Gohr     * @param $rand  float  some random number between 0 and 1
20723d379a8SAndreas Gohr     * @return string
20877e00bf9SAndreas Gohr     */
20909b1e97eSAndreas Gohr    public function generateCaptchaCode($ident, $rand)
21018622736SAndreas Gohr    {
21109b1e97eSAndreas Gohr        $numbers = $this->generateMagicCode($ident, $rand);
21277e00bf9SAndreas Gohr
21377e00bf9SAndreas Gohr        // now create the letters
21477e00bf9SAndreas Gohr        $code = '';
2159a516edaSPatrick Brown        $lettercount = $this->getConf('lettercount') * 2;
2169a516edaSPatrick Brown        if ($lettercount > strlen($numbers)) $lettercount = strlen($numbers);
2179a516edaSPatrick Brown        for ($i = 0; $i < $lettercount; $i += 2) {
21877e00bf9SAndreas Gohr            $code .= chr(floor(hexdec($numbers[$i] . $numbers[$i + 1]) / 10) + 65);
21977e00bf9SAndreas Gohr        }
22077e00bf9SAndreas Gohr
22177e00bf9SAndreas Gohr        return $code;
22277e00bf9SAndreas Gohr    }
22377e00bf9SAndreas Gohr
2249e312724SAndreas Gohr    /**
2259e312724SAndreas Gohr     * Create a mathematical task and its result
2269e312724SAndreas Gohr     *
22709b1e97eSAndreas Gohr     * @param $ident string the fixed part, any string
228104ec268SAndreas Gohr     * @param $rand  float  some random number between 0 and 1
22909b1e97eSAndreas Gohr     * @return array [task, result]
2309e312724SAndreas Gohr     */
23109b1e97eSAndreas Gohr    protected function generateMath($ident, $rand)
23218622736SAndreas Gohr    {
23309b1e97eSAndreas Gohr        $numbers = $this->generateMagicCode($ident, $rand);
2349e312724SAndreas Gohr
2359e312724SAndreas Gohr        // first letter is the operator (+/-)
2369e312724SAndreas Gohr        $op = (hexdec($numbers[0]) > 8) ? -1 : 1;
23709b1e97eSAndreas Gohr        $num = [hexdec($numbers[1] . $numbers[2]), hexdec($numbers[3])];
2389e312724SAndreas Gohr
2399e312724SAndreas Gohr        // we only want positive results
2409e312724SAndreas Gohr        if (($op < 0) && ($num[0] < $num[1])) rsort($num);
2419e312724SAndreas Gohr
2429e312724SAndreas Gohr        // prepare result and task text
2439e312724SAndreas Gohr        $res = $num[0] + ($num[1] * $op);
2449bc1fab2SApostolos P. Tsompanopoulos        $task = $num[0] . (($op < 0) ? '-' : '+') . $num[1] . '= ';
2459e312724SAndreas Gohr
24609b1e97eSAndreas Gohr        return [$task, $res];
2479e312724SAndreas Gohr    }
24877e00bf9SAndreas Gohr
24909b1e97eSAndreas Gohr    // endregion
25009b1e97eSAndreas Gohr
25109b1e97eSAndreas Gohr    // region Output Builders
25209b1e97eSAndreas Gohr
25377e00bf9SAndreas Gohr    /**
25477e00bf9SAndreas Gohr     * Create a CAPTCHA image
255104ec268SAndreas Gohr     *
256104ec268SAndreas Gohr     * @param string $text the letters to display
25709b1e97eSAndreas Gohr     * @return string The image data
25877e00bf9SAndreas Gohr     */
25909b1e97eSAndreas Gohr    public function imageCaptcha($text)
26018622736SAndreas Gohr    {
26177e00bf9SAndreas Gohr        $w = $this->getConf('width');
26277e00bf9SAndreas Gohr        $h = $this->getConf('height');
26377e00bf9SAndreas Gohr
26409b1e97eSAndreas Gohr        $fonts = glob(__DIR__ . '/fonts/*.ttf');
265dc091fd0SAndreas Gohr
26677e00bf9SAndreas Gohr        // create a white image
26728c14643SAndreas Gohr        $img = imagecreatetruecolor($w, $h);
26828c14643SAndreas Gohr        $white = imagecolorallocate($img, 255, 255, 255);
26928c14643SAndreas Gohr        imagefill($img, 0, 0, $white);
27077e00bf9SAndreas Gohr
27177e00bf9SAndreas Gohr        // add some lines as background noise
27277e00bf9SAndreas Gohr        for ($i = 0; $i < 30; $i++) {
27309b1e97eSAndreas Gohr            $color = imagecolorallocate($img, random_int(100, 250), random_int(100, 250), random_int(100, 250));
27409b1e97eSAndreas Gohr            imageline($img, random_int(0, $w), random_int(0, $h), random_int(0, $w), random_int(0, $h), $color);
27577e00bf9SAndreas Gohr        }
27677e00bf9SAndreas Gohr
27777e00bf9SAndreas Gohr        // draw the letters
27828c14643SAndreas Gohr        $txtlen = strlen($text);
27928c14643SAndreas Gohr        for ($i = 0; $i < $txtlen; $i++) {
280dc091fd0SAndreas Gohr            $font = $fonts[array_rand($fonts)];
28109b1e97eSAndreas Gohr            $color = imagecolorallocate($img, random_int(0, 100), random_int(0, 100), random_int(0, 100));
28209b1e97eSAndreas Gohr            $size = random_int(floor($h / 1.8), floor($h * 0.7));
28309b1e97eSAndreas Gohr            $angle = random_int(-35, 35);
28477e00bf9SAndreas Gohr
28528c14643SAndreas Gohr            $x = ($w * 0.05) + $i * floor($w * 0.9 / $txtlen);
28677e00bf9SAndreas Gohr            $cheight = $size + ($size * 0.5);
28777e00bf9SAndreas Gohr            $y = floor($h / 2 + $cheight / 3.8);
28877e00bf9SAndreas Gohr
28977e00bf9SAndreas Gohr            imagettftext($img, $size, $angle, $x, $y, $color, $font, $text[$i]);
29077e00bf9SAndreas Gohr        }
29177e00bf9SAndreas Gohr
29209b1e97eSAndreas Gohr        ob_start();
29377e00bf9SAndreas Gohr        imagepng($img);
29409b1e97eSAndreas Gohr        $image = ob_get_clean();
29577e00bf9SAndreas Gohr        imagedestroy($img);
29609b1e97eSAndreas Gohr        return $image;
29708f248e4SAndreas Gohr    }
29808f248e4SAndreas Gohr
29908f248e4SAndreas Gohr    /**
30063609b6eSAndreas Gohr     * Generate an audio captcha
30163609b6eSAndreas Gohr     *
30263609b6eSAndreas Gohr     * @param string $text
30309b1e97eSAndreas Gohr     * @return string The joined wav files
30463609b6eSAndreas Gohr     */
30509b1e97eSAndreas Gohr    public function audioCaptcha($text)
30609b1e97eSAndreas Gohr    {
30763609b6eSAndreas Gohr        global $conf;
30863609b6eSAndreas Gohr
30963609b6eSAndreas Gohr        $lc = __DIR__ . '/lang/' . $conf['lang'] . '/audio/';
31063609b6eSAndreas Gohr        $en = __DIR__ . '/lang/en/audio/';
31163609b6eSAndreas Gohr
31263609b6eSAndreas Gohr        $wavs = [];
31363609b6eSAndreas Gohr
31463609b6eSAndreas Gohr        $text = strtolower($text);
31563609b6eSAndreas Gohr        $txtlen = strlen($text);
31663609b6eSAndreas Gohr        for ($i = 0; $i < $txtlen; $i++) {
31763609b6eSAndreas Gohr            $char = $text[$i];
31863609b6eSAndreas Gohr            $file = $lc . $char . '.wav';
31963609b6eSAndreas Gohr            if (!@file_exists($file)) $file = $en . $char . '.wav';
32063609b6eSAndreas Gohr            $wavs[] = $file;
32163609b6eSAndreas Gohr        }
32263609b6eSAndreas Gohr
32309b1e97eSAndreas Gohr        return $this->joinwavs($wavs);
32463609b6eSAndreas Gohr    }
32563609b6eSAndreas Gohr
32663609b6eSAndreas Gohr    /**
32709b1e97eSAndreas Gohr     * Create an SVG of the given text
32809b1e97eSAndreas Gohr     *
32909b1e97eSAndreas Gohr     * @param string $text
33009b1e97eSAndreas Gohr     * @return string
33109b1e97eSAndreas Gohr     */
33209b1e97eSAndreas Gohr    public function svgCaptcha($text)
33309b1e97eSAndreas Gohr    {
33409b1e97eSAndreas Gohr        require_once(__DIR__ . '/EasySVG.php');
33509b1e97eSAndreas Gohr
33609b1e97eSAndreas Gohr        $fonts = glob(__DIR__ . '/fonts/*.svg');
33709b1e97eSAndreas Gohr
33809b1e97eSAndreas Gohr        $x = 0; // where we start to draw
33909b1e97eSAndreas Gohr        $y = 100; // our max height
34009b1e97eSAndreas Gohr
34109b1e97eSAndreas Gohr        $svg = new EasySVG();
34209b1e97eSAndreas Gohr
34309b1e97eSAndreas Gohr        // draw the letters
34409b1e97eSAndreas Gohr        $txtlen = strlen($text);
34509b1e97eSAndreas Gohr        for ($i = 0; $i < $txtlen; $i++) {
34609b1e97eSAndreas Gohr            $char = $text[$i];
34709b1e97eSAndreas Gohr            $size = random_int($y / 2, $y - $y * 0.1); // 50-90%
34809b1e97eSAndreas Gohr            $svg->setFontSVG($fonts[array_rand($fonts)]);
34909b1e97eSAndreas Gohr
35009b1e97eSAndreas Gohr            $svg->setFontSize($size);
35109b1e97eSAndreas Gohr            $svg->setLetterSpacing(round(random_int(1, 4) / 10, 2)); // 0.1 - 0.4
35209b1e97eSAndreas Gohr            $svg->addText($char, $x, random_int(0, round($y - $size))); // random up and down
35309b1e97eSAndreas Gohr
35409b1e97eSAndreas Gohr            [$w] = $svg->textDimensions($char);
35509b1e97eSAndreas Gohr            $x += $w;
35609b1e97eSAndreas Gohr        }
35709b1e97eSAndreas Gohr
35809b1e97eSAndreas Gohr        $svg->addAttribute('width', $x . 'px');
35909b1e97eSAndreas Gohr        $svg->addAttribute('height', $y . 'px');
36009b1e97eSAndreas Gohr        $svg->addAttribute('viewbox', "0 0 $x $y");
36109b1e97eSAndreas Gohr        return $svg->asXML();
36209b1e97eSAndreas Gohr    }
36309b1e97eSAndreas Gohr
364*1cd9cde7SAndreas Gohr    /**
365*1cd9cde7SAndreas Gohr     * Inline SVG showing the given code
366*1cd9cde7SAndreas Gohr     *
367*1cd9cde7SAndreas Gohr     * @param string $code
368*1cd9cde7SAndreas Gohr     * @return string
369*1cd9cde7SAndreas Gohr     */
370*1cd9cde7SAndreas Gohr    protected function htmlSvg($code)
371*1cd9cde7SAndreas Gohr    {
372*1cd9cde7SAndreas Gohr        return sprintf(
373*1cd9cde7SAndreas Gohr            '<span class="svg" style="width:%spx; height:%spx">%s</span>',
374*1cd9cde7SAndreas Gohr            $this->getConf('width'),
375*1cd9cde7SAndreas Gohr            $this->getConf('height'),
376*1cd9cde7SAndreas Gohr            $this->svgCaptcha($code)
377*1cd9cde7SAndreas Gohr        );
378*1cd9cde7SAndreas Gohr    }
379*1cd9cde7SAndreas Gohr
380*1cd9cde7SAndreas Gohr    /**
381*1cd9cde7SAndreas Gohr     * HTML for an img tag for the image captcha
382*1cd9cde7SAndreas Gohr     *
383*1cd9cde7SAndreas Gohr     * @param string $ID the page ID this is displayed on
384*1cd9cde7SAndreas Gohr     * @param string $secret the encrypted random number
385*1cd9cde7SAndreas Gohr     * @return string
386*1cd9cde7SAndreas Gohr     */
387*1cd9cde7SAndreas Gohr    protected function htmlImage($ID, $secret)
388*1cd9cde7SAndreas Gohr    {
389*1cd9cde7SAndreas Gohr        $img = DOKU_BASE . 'lib/plugins/captcha/img.php';
390*1cd9cde7SAndreas Gohr        $param = buildURLparams([
391*1cd9cde7SAndreas Gohr            'secret' => $secret,
392*1cd9cde7SAndreas Gohr            'id' => $ID,
393*1cd9cde7SAndreas Gohr        ]);
394*1cd9cde7SAndreas Gohr
395*1cd9cde7SAndreas Gohr        return sprintf(
396*1cd9cde7SAndreas Gohr            '<img src="%s?%s" width="%d" height="%d" alt="" />',
397*1cd9cde7SAndreas Gohr            $img,
398*1cd9cde7SAndreas Gohr            $param,
399*1cd9cde7SAndreas Gohr            $this->getConf('width'),
400*1cd9cde7SAndreas Gohr            $this->getConf('height')
401*1cd9cde7SAndreas Gohr        );
402*1cd9cde7SAndreas Gohr    }
403*1cd9cde7SAndreas Gohr
404*1cd9cde7SAndreas Gohr    /**
405*1cd9cde7SAndreas Gohr     * HTML for a link to the audio captcha
406*1cd9cde7SAndreas Gohr     *
407*1cd9cde7SAndreas Gohr     * @param string $secret the encrypted random number
408*1cd9cde7SAndreas Gohr     * @param string $ID the page ID this is displayed on
409*1cd9cde7SAndreas Gohr     * @return string
410*1cd9cde7SAndreas Gohr     */
411*1cd9cde7SAndreas Gohr    protected function htmlAudioLink($secret, $ID)
412*1cd9cde7SAndreas Gohr    {
413*1cd9cde7SAndreas Gohr        $img = DOKU_BASE . 'lib/plugins/captcha/sound.png'; // FIXME use svg icon
414*1cd9cde7SAndreas Gohr        $url = DOKU_BASE . 'lib/plugins/captcha/wav.php';
415*1cd9cde7SAndreas Gohr        $param = buildURLparams([
416*1cd9cde7SAndreas Gohr            'secret' => $secret,
417*1cd9cde7SAndreas Gohr            'id' => $ID,
418*1cd9cde7SAndreas Gohr        ]);
419*1cd9cde7SAndreas Gohr
420*1cd9cde7SAndreas Gohr        $icon = sprintf(
421*1cd9cde7SAndreas Gohr            '<img src="%s" width="16" height="16" alt="%s" /></a>',
422*1cd9cde7SAndreas Gohr            $img,
423*1cd9cde7SAndreas Gohr            $this->getLang('soundlink')
424*1cd9cde7SAndreas Gohr        );
425*1cd9cde7SAndreas Gohr
426*1cd9cde7SAndreas Gohr        return sprintf(
427*1cd9cde7SAndreas Gohr            '<a href="%s?%s" class="JSnocheck audiolink" title="%s">%s</a>',
428*1cd9cde7SAndreas Gohr            $url,
429*1cd9cde7SAndreas Gohr            $param,
430*1cd9cde7SAndreas Gohr            $this->getLang('soundlink'),
431*1cd9cde7SAndreas Gohr            $icon
432*1cd9cde7SAndreas Gohr        );
433*1cd9cde7SAndreas Gohr    }
434*1cd9cde7SAndreas Gohr
435*1cd9cde7SAndreas Gohr    /**
436*1cd9cde7SAndreas Gohr     * The HTML to show a figlet captcha
437*1cd9cde7SAndreas Gohr     *
438*1cd9cde7SAndreas Gohr     * @param string $code the code to display
439*1cd9cde7SAndreas Gohr     * @return string
440*1cd9cde7SAndreas Gohr     */
441*1cd9cde7SAndreas Gohr    protected function htmlFiglet($code)
442*1cd9cde7SAndreas Gohr    {
443*1cd9cde7SAndreas Gohr        require_once(__DIR__ . '/figlet.php');
444*1cd9cde7SAndreas Gohr        $figlet = new phpFiglet();
445*1cd9cde7SAndreas Gohr        if ($figlet->loadfont(__DIR__ . '/figlet.flf')) {
446*1cd9cde7SAndreas Gohr            return '<pre>' . rtrim($figlet->fetch($code)) . '</pre>';
447*1cd9cde7SAndreas Gohr        } else {
448*1cd9cde7SAndreas Gohr            msg('Failed to load figlet.flf font file. CAPTCHA broken', -1);
449*1cd9cde7SAndreas Gohr        }
450*1cd9cde7SAndreas Gohr        return 'FAIL';
451*1cd9cde7SAndreas Gohr    }
452*1cd9cde7SAndreas Gohr
45309b1e97eSAndreas Gohr    // endregion
45409b1e97eSAndreas Gohr
45509b1e97eSAndreas Gohr    // region Utilities
45609b1e97eSAndreas Gohr
45709b1e97eSAndreas Gohr    /**
458f044313dSAndreas Gohr     * Encrypt the given string with the cookie salt
459f044313dSAndreas Gohr     *
460f044313dSAndreas Gohr     * @param string $data
461f044313dSAndreas Gohr     * @return string
462f044313dSAndreas Gohr     */
46318622736SAndreas Gohr    public function encrypt($data)
46418622736SAndreas Gohr    {
46509b1e97eSAndreas Gohr        $data = auth_encrypt($data, auth_cookiesalt());
466f044313dSAndreas Gohr        return base64_encode($data);
467f044313dSAndreas Gohr    }
468f044313dSAndreas Gohr
469f044313dSAndreas Gohr    /**
470f044313dSAndreas Gohr     * Decrypt the given string with the cookie salt
471f044313dSAndreas Gohr     *
472f044313dSAndreas Gohr     * @param string $data
473f044313dSAndreas Gohr     * @return string
474f044313dSAndreas Gohr     */
47518622736SAndreas Gohr    public function decrypt($data)
47618622736SAndreas Gohr    {
477f044313dSAndreas Gohr        $data = base64_decode($data);
47809870f99SPatrick Brown        if ($data === false || $data === '') return false;
479f044313dSAndreas Gohr
48009b1e97eSAndreas Gohr        return auth_decrypt($data, auth_cookiesalt());
481f044313dSAndreas Gohr    }
48209b1e97eSAndreas Gohr
48309b1e97eSAndreas Gohr    /**
48409b1e97eSAndreas Gohr     * Adds random space characters within the given text
48509b1e97eSAndreas Gohr     *
48609b1e97eSAndreas Gohr     * Keeps subsequent numbers without spaces (for math problem)
48709b1e97eSAndreas Gohr     *
48809b1e97eSAndreas Gohr     * @param $text
48909b1e97eSAndreas Gohr     * @return string
49009b1e97eSAndreas Gohr     */
49109b1e97eSAndreas Gohr    protected function obfuscateText($text)
49209b1e97eSAndreas Gohr    {
49309b1e97eSAndreas Gohr        $new = '';
49409b1e97eSAndreas Gohr
49509b1e97eSAndreas Gohr        $spaces = [
49609b1e97eSAndreas Gohr            "\r",
49709b1e97eSAndreas Gohr            "\n",
49809b1e97eSAndreas Gohr            "\r\n",
49909b1e97eSAndreas Gohr            ' ',
50009b1e97eSAndreas Gohr            "\xC2\xA0",
50109b1e97eSAndreas Gohr            // \u00A0    NO-BREAK SPACE
50209b1e97eSAndreas Gohr            "\xE2\x80\x80",
50309b1e97eSAndreas Gohr            // \u2000    EN QUAD
50409b1e97eSAndreas Gohr            "\xE2\x80\x81",
50509b1e97eSAndreas Gohr            // \u2001    EM QUAD
50609b1e97eSAndreas Gohr            "\xE2\x80\x82",
50709b1e97eSAndreas Gohr            // \u2002    EN SPACE
50809b1e97eSAndreas Gohr            //         "\xE2\x80\x83", // \u2003    EM SPACE
50909b1e97eSAndreas Gohr            "\xE2\x80\x84",
51009b1e97eSAndreas Gohr            // \u2004    THREE-PER-EM SPACE
51109b1e97eSAndreas Gohr            "\xE2\x80\x85",
51209b1e97eSAndreas Gohr            // \u2005    FOUR-PER-EM SPACE
51309b1e97eSAndreas Gohr            "\xE2\x80\x86",
51409b1e97eSAndreas Gohr            // \u2006    SIX-PER-EM SPACE
51509b1e97eSAndreas Gohr            "\xE2\x80\x87",
51609b1e97eSAndreas Gohr            // \u2007    FIGURE SPACE
51709b1e97eSAndreas Gohr            "\xE2\x80\x88",
51809b1e97eSAndreas Gohr            // \u2008    PUNCTUATION SPACE
51909b1e97eSAndreas Gohr            "\xE2\x80\x89",
52009b1e97eSAndreas Gohr            // \u2009    THIN SPACE
52109b1e97eSAndreas Gohr            "\xE2\x80\x8A",
52209b1e97eSAndreas Gohr            // \u200A    HAIR SPACE
52309b1e97eSAndreas Gohr            "\xE2\x80\xAF",
52409b1e97eSAndreas Gohr            // \u202F    NARROW NO-BREAK SPACE
52509b1e97eSAndreas Gohr            "\xE2\x81\x9F",
52609b1e97eSAndreas Gohr            // \u205F    MEDIUM MATHEMATICAL SPACE
52709b1e97eSAndreas Gohr            "\xE1\xA0\x8E\r\n",
52809b1e97eSAndreas Gohr            // \u180E    MONGOLIAN VOWEL SEPARATOR
52909b1e97eSAndreas Gohr            "\xE2\x80\x8B\r\n",
53009b1e97eSAndreas Gohr            // \u200B    ZERO WIDTH SPACE
53109b1e97eSAndreas Gohr            "\xEF\xBB\xBF\r\n",
53209b1e97eSAndreas Gohr        ];
53309b1e97eSAndreas Gohr
53409b1e97eSAndreas Gohr        $len = strlen($text);
53509b1e97eSAndreas Gohr        for ($i = 0; $i < $len - 1; $i++) {
53609b1e97eSAndreas Gohr            $new .= $text[$i];
53709b1e97eSAndreas Gohr
53809b1e97eSAndreas Gohr            if (!is_numeric($text[$i + 1])) {
53909b1e97eSAndreas Gohr                $new .= $spaces[array_rand($spaces)];
54009b1e97eSAndreas Gohr            }
54109b1e97eSAndreas Gohr        }
54209b1e97eSAndreas Gohr        $new .= $text[$len - 1];
54309b1e97eSAndreas Gohr        return $new;
544f044313dSAndreas Gohr    }
545969b14c4SAndreas Gohr
54663609b6eSAndreas Gohr
54763609b6eSAndreas Gohr    /**
54863609b6eSAndreas Gohr     * Join multiple wav files
54963609b6eSAndreas Gohr     *
55063609b6eSAndreas Gohr     * All wave files need to have the same format and need to be uncompressed.
55163609b6eSAndreas Gohr     * The headers of the last file will be used (with recalculated datasize
55263609b6eSAndreas Gohr     * of course)
55363609b6eSAndreas Gohr     *
55463609b6eSAndreas Gohr     * @link http://ccrma.stanford.edu/CCRMA/Courses/422/projects/WaveFormat/
55563609b6eSAndreas Gohr     * @link http://www.thescripts.com/forum/thread3770.html
55663609b6eSAndreas Gohr     */
55763609b6eSAndreas Gohr    protected function joinwavs($wavs)
55863609b6eSAndreas Gohr    {
55909b1e97eSAndreas Gohr        $fields = implode(
56009b1e97eSAndreas Gohr            '/',
56109b1e97eSAndreas Gohr            [
56263609b6eSAndreas Gohr                'H8ChunkID',
56363609b6eSAndreas Gohr                'VChunkSize',
56463609b6eSAndreas Gohr                'H8Format',
56563609b6eSAndreas Gohr                'H8Subchunk1ID',
56663609b6eSAndreas Gohr                'VSubchunk1Size',
56763609b6eSAndreas Gohr                'vAudioFormat',
56863609b6eSAndreas Gohr                'vNumChannels',
56963609b6eSAndreas Gohr                'VSampleRate',
57063609b6eSAndreas Gohr                'VByteRate',
57163609b6eSAndreas Gohr                'vBlockAlign',
57209b1e97eSAndreas Gohr                'vBitsPerSample'
57309b1e97eSAndreas Gohr            ]
57463609b6eSAndreas Gohr        );
57563609b6eSAndreas Gohr
57663609b6eSAndreas Gohr        $data = '';
57763609b6eSAndreas Gohr        foreach ($wavs as $wav) {
57863609b6eSAndreas Gohr            $fp = fopen($wav, 'rb');
57963609b6eSAndreas Gohr            $header = fread($fp, 36);
58063609b6eSAndreas Gohr            $info = unpack($fields, $header);
58163609b6eSAndreas Gohr
58263609b6eSAndreas Gohr            // read optional extra stuff
58363609b6eSAndreas Gohr            if ($info['Subchunk1Size'] > 16) {
58463609b6eSAndreas Gohr                $header .= fread($fp, ($info['Subchunk1Size'] - 16));
58563609b6eSAndreas Gohr            }
58663609b6eSAndreas Gohr
58763609b6eSAndreas Gohr            // read SubChunk2ID
58863609b6eSAndreas Gohr            $header .= fread($fp, 4);
58963609b6eSAndreas Gohr
59063609b6eSAndreas Gohr            // read Subchunk2Size
59163609b6eSAndreas Gohr            $size = unpack('vsize', fread($fp, 4));
59263609b6eSAndreas Gohr            $size = $size['size'];
59363609b6eSAndreas Gohr
59463609b6eSAndreas Gohr            // read data
59563609b6eSAndreas Gohr            $data .= fread($fp, $size);
59663609b6eSAndreas Gohr        }
59763609b6eSAndreas Gohr
59863609b6eSAndreas Gohr        return $header . pack('V', strlen($data)) . $data;
59963609b6eSAndreas Gohr    }
60063609b6eSAndreas Gohr
60109b1e97eSAndreas Gohr    // endregion
60277e00bf9SAndreas Gohr}
603