xref: /plugin/captcha/helper.php (revision c6d794b3c9d4f058a62b1ade70316fe9384e4827)
177e00bf9SAndreas Gohr<?php
209b1e97eSAndreas Gohr
309b1e97eSAndreas Gohruse dokuwiki\Extension\Plugin;
4*c6d794b3SAndreas 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;
55*c6d794b3SAndreas Gohr        $cookie = new FileCookie($this->fixedIdent(), $rand);
56*c6d794b3SAndreas 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':
8409b1e97eSAndreas Gohr                $out .= '<span id="plugin__captcha_code">' . $this->obfuscateText($code) . '</span>';
8577e00bf9SAndreas Gohr                break;
8608f248e4SAndreas Gohr            case 'svg':
8708f248e4SAndreas Gohr                $out .= '<span class="svg" style="width:' . $this->getConf('width') . 'px; height:' . $this->getConf('height') . 'px">';
8809b1e97eSAndreas Gohr                $out .= $this->svgCaptcha($code);
8908f248e4SAndreas Gohr                $out .= '</span>';
9008f248e4SAndreas Gohr                break;
9108f248e4SAndreas Gohr            case 'svgaudio':
9208f248e4SAndreas Gohr                $out .= '<span class="svg" style="width:' . $this->getConf('width') . 'px; height:' . $this->getConf('height') . 'px">';
9309b1e97eSAndreas Gohr                $out .= $this->svgCaptcha($code);
9408f248e4SAndreas Gohr                $out .= '</span>';
9508f248e4SAndreas Gohr                $out .= '<a href="' . DOKU_BASE . 'lib/plugins/captcha/wav.php?secret=' . rawurlencode($secret) . '&amp;id=' . $ID . '"' .
969efb703bSStefan Bethke                    ' class="JSnocheck audiolink" title="' . $this->getLang('soundlink') . '">';
9708f248e4SAndreas Gohr                $out .= '<img src="' . DOKU_BASE . 'lib/plugins/captcha/sound.png" width="16" height="16"' .
9808f248e4SAndreas Gohr                    ' alt="' . $this->getLang('soundlink') . '" /></a>';
9908f248e4SAndreas Gohr                break;
10077e00bf9SAndreas Gohr            case 'image':
10177e00bf9SAndreas Gohr                $out .= '<img src="' . DOKU_BASE . 'lib/plugins/captcha/img.php?secret=' . rawurlencode($secret) . '&amp;id=' . $ID . '" ' .
10277e00bf9SAndreas Gohr                    ' width="' . $this->getConf('width') . '" height="' . $this->getConf('height') . '" alt="" /> ';
10377e00bf9SAndreas Gohr                break;
10477e00bf9SAndreas Gohr            case 'audio':
10577e00bf9SAndreas Gohr                $out .= '<img src="' . DOKU_BASE . 'lib/plugins/captcha/img.php?secret=' . rawurlencode($secret) . '&amp;id=' . $ID . '" ' .
10677e00bf9SAndreas Gohr                    ' width="' . $this->getConf('width') . '" height="' . $this->getConf('height') . '" alt="" /> ';
10777e00bf9SAndreas Gohr                $out .= '<a href="' . DOKU_BASE . 'lib/plugins/captcha/wav.php?secret=' . rawurlencode($secret) . '&amp;id=' . $ID . '"' .
1089efb703bSStefan Bethke                    ' class="JSnocheck audiolink" title="' . $this->getLang('soundlink') . '">';
10977e00bf9SAndreas Gohr                $out .= '<img src="' . DOKU_BASE . 'lib/plugins/captcha/sound.png" width="16" height="16"' .
11077e00bf9SAndreas Gohr                    ' alt="' . $this->getLang('soundlink') . '" /></a>';
11177e00bf9SAndreas Gohr                break;
11252e95008SAndreas Gohr            case 'figlet':
11309b1e97eSAndreas Gohr                require_once(__DIR__ . '/figlet.php');
11452e95008SAndreas Gohr                $figlet = new phpFiglet();
11509b1e97eSAndreas Gohr                if ($figlet->loadfont(__DIR__ . '/figlet.flf')) {
11652e95008SAndreas Gohr                    $out .= '<pre>';
11752e95008SAndreas Gohr                    $out .= rtrim($figlet->fetch($code));
11852e95008SAndreas Gohr                    $out .= '</pre>';
11952e95008SAndreas Gohr                } else {
12052e95008SAndreas Gohr                    msg('Failed to load figlet.flf font file. CAPTCHA broken', -1);
12152e95008SAndreas Gohr                }
12252e95008SAndreas Gohr                break;
12377e00bf9SAndreas Gohr        }
124df8afac4SAndreas Gohr        $out .= ' <input type="text" size="' . $txtlen . '" name="' . $this->field_in . '" class="edit" /> ';
12523d379a8SAndreas Gohr
12623d379a8SAndreas Gohr        // add honeypot field
1279d63c05fSlainme        $out .= '<label class="no">' . $this->getLang('honeypot') . '<input type="text" name="' . $this->field_hp . '" /></label>';
12877e00bf9SAndreas Gohr        $out .= '</div>';
12977e00bf9SAndreas Gohr        return $out;
13077e00bf9SAndreas Gohr    }
13177e00bf9SAndreas Gohr
13277e00bf9SAndreas Gohr    /**
13318622736SAndreas Gohr     * Checks if the CAPTCHA was solved correctly
13477e00bf9SAndreas Gohr     *
13577e00bf9SAndreas Gohr     * @param bool $msg when true, an error will be signalled through the msg() method
13677e00bf9SAndreas Gohr     * @return bool true when the answer was correct, otherwise false
13777e00bf9SAndreas Gohr     */
13818622736SAndreas Gohr    public function check($msg = true)
13918622736SAndreas Gohr    {
140478e363cSAndreas Gohr        global $INPUT;
141478e363cSAndreas Gohr
142478e363cSAndreas Gohr        $field_sec = $INPUT->str($this->field_sec);
143478e363cSAndreas Gohr        $field_in = $INPUT->str($this->field_in);
144478e363cSAndreas Gohr        $field_hp = $INPUT->str($this->field_hp);
145478e363cSAndreas Gohr
146478e363cSAndreas Gohr        // reconstruct captcha from provided $field_sec
147478e363cSAndreas Gohr        $rand = $this->decrypt($field_sec);
1489e312724SAndreas Gohr
1499e312724SAndreas Gohr        if ($this->getConf('mode') == 'math') {
15009b1e97eSAndreas Gohr            $code = $this->generateMath($this->fixedIdent(), $rand);
1519e312724SAndreas Gohr            $code = $code[1];
152df8afac4SAndreas Gohr        } elseif ($this->getConf('mode') == 'question') {
153df8afac4SAndreas Gohr            $code = $this->getConf('answer');
1549e312724SAndreas Gohr        } else {
15509b1e97eSAndreas Gohr            $code = $this->generateCaptchaCode($this->fixedIdent(), $rand);
1569e312724SAndreas Gohr        }
15777e00bf9SAndreas Gohr
158478e363cSAndreas Gohr        // compare values
15909b1e97eSAndreas Gohr        if (
16009b1e97eSAndreas Gohr            !$field_sec ||
161478e363cSAndreas Gohr            !$field_in ||
16214e271ebSPatrick Brown            $rand === false ||
16309b1e97eSAndreas Gohr            PhpString::strtolower($field_in) != PhpString::strtolower($code) ||
164a285df67SAndreas Gohr            trim($field_hp) !== '' ||
165*c6d794b3SAndreas Gohr            !(new FileCookie($this->fixedIdent(), $rand))->check()
16623d379a8SAndreas Gohr        ) {
16777e00bf9SAndreas Gohr            if ($msg) msg($this->getLang('testfailed'), -1);
16877e00bf9SAndreas Gohr            return false;
16977e00bf9SAndreas Gohr        }
17077e00bf9SAndreas Gohr        return true;
17177e00bf9SAndreas Gohr    }
17277e00bf9SAndreas Gohr
17309b1e97eSAndreas Gohr    // endregion
17409b1e97eSAndreas Gohr
17509b1e97eSAndreas Gohr    // region Captcha Generation methods
17609b1e97eSAndreas Gohr
177a285df67SAndreas Gohr    /**
17877e00bf9SAndreas Gohr     * Build a semi-secret fixed string identifying the current page and user
17977e00bf9SAndreas Gohr     *
18077e00bf9SAndreas Gohr     * This string is always the same for the current user when editing the same
18127d84d8dSAndreas Gohr     * page revision, but only for one day. Editing a page before midnight and saving
18227d84d8dSAndreas Gohr     * after midnight will result in a failed CAPTCHA once, but makes sure it can
18327d84d8dSAndreas Gohr     * not be reused which is especially important for the registration form where the
18427d84d8dSAndreas Gohr     * $ID usually won't change.
185104ec268SAndreas Gohr     *
186104ec268SAndreas Gohr     * @return string
18777e00bf9SAndreas Gohr     */
18809b1e97eSAndreas Gohr    public function fixedIdent()
18918622736SAndreas Gohr    {
19077e00bf9SAndreas Gohr        global $ID;
19177e00bf9SAndreas Gohr        $lm = @filemtime(wikiFN($ID));
19227d84d8dSAndreas Gohr        $td = date('Y-m-d');
19363609b6eSAndreas Gohr        $ip = clientIP();
19463609b6eSAndreas Gohr        $salt = auth_cookiesalt();
19563609b6eSAndreas Gohr
19609b1e97eSAndreas Gohr        return sha1(implode("\n", [$ID, $lm, $td, $ip, $salt]));
19777e00bf9SAndreas Gohr    }
19877e00bf9SAndreas Gohr
19977e00bf9SAndreas Gohr    /**
20009b1e97eSAndreas Gohr     * Generate a magic code based on the given data
2019d6f09afSAndreas Gohr     *
20209b1e97eSAndreas Gohr     * This "magic" code represents the given fixed identifier (see fixedIdent()) and the given
20309b1e97eSAndreas Gohr     * random number. It is used to generate the actual CAPTCHA code.
2049d6f09afSAndreas Gohr     *
20509b1e97eSAndreas Gohr     * @param $ident string the fixed part, any string
206a02b2219SPatrick Brown     * @param $rand  float  some random number between 0 and 1
207a02b2219SPatrick Brown     * @return string
208a02b2219SPatrick Brown     */
20909b1e97eSAndreas Gohr    protected function generateMagicCode($ident, $rand)
21018622736SAndreas Gohr    {
21109b1e97eSAndreas Gohr        $ident = hexdec(substr(md5($ident), 5, 5)); // use part of the md5 to generate an int
21209b1e97eSAndreas Gohr        $rand *= 0xFFFFF; // bitmask from the random number
21309b1e97eSAndreas Gohr        return md5($rand ^ $ident); // combine both values
214a02b2219SPatrick Brown    }
215a02b2219SPatrick Brown
216a02b2219SPatrick Brown    /**
21709b1e97eSAndreas Gohr     * Generates a char string based on the given data
21877e00bf9SAndreas Gohr     *
21909b1e97eSAndreas Gohr     * The string is pseudo random based on a fixed identifier (see fixedIdent()) and a random number.
22009b1e97eSAndreas Gohr     *
22109b1e97eSAndreas Gohr     * @param $ident string the fixed part, any string
222104ec268SAndreas Gohr     * @param $rand  float  some random number between 0 and 1
22323d379a8SAndreas Gohr     * @return string
22477e00bf9SAndreas Gohr     */
22509b1e97eSAndreas Gohr    public function generateCaptchaCode($ident, $rand)
22618622736SAndreas Gohr    {
22709b1e97eSAndreas Gohr        $numbers = $this->generateMagicCode($ident, $rand);
22877e00bf9SAndreas Gohr
22977e00bf9SAndreas Gohr        // now create the letters
23077e00bf9SAndreas Gohr        $code = '';
2319a516edaSPatrick Brown        $lettercount = $this->getConf('lettercount') * 2;
2329a516edaSPatrick Brown        if ($lettercount > strlen($numbers)) $lettercount = strlen($numbers);
2339a516edaSPatrick Brown        for ($i = 0; $i < $lettercount; $i += 2) {
23477e00bf9SAndreas Gohr            $code .= chr(floor(hexdec($numbers[$i] . $numbers[$i + 1]) / 10) + 65);
23577e00bf9SAndreas Gohr        }
23677e00bf9SAndreas Gohr
23777e00bf9SAndreas Gohr        return $code;
23877e00bf9SAndreas Gohr    }
23977e00bf9SAndreas Gohr
2409e312724SAndreas Gohr    /**
2419e312724SAndreas Gohr     * Create a mathematical task and its result
2429e312724SAndreas Gohr     *
24309b1e97eSAndreas Gohr     * @param $ident string the fixed part, any string
244104ec268SAndreas Gohr     * @param $rand  float  some random number between 0 and 1
24509b1e97eSAndreas Gohr     * @return array [task, result]
2469e312724SAndreas Gohr     */
24709b1e97eSAndreas Gohr    protected function generateMath($ident, $rand)
24818622736SAndreas Gohr    {
24909b1e97eSAndreas Gohr        $numbers = $this->generateMagicCode($ident, $rand);
2509e312724SAndreas Gohr
2519e312724SAndreas Gohr        // first letter is the operator (+/-)
2529e312724SAndreas Gohr        $op = (hexdec($numbers[0]) > 8) ? -1 : 1;
25309b1e97eSAndreas Gohr        $num = [hexdec($numbers[1] . $numbers[2]), hexdec($numbers[3])];
2549e312724SAndreas Gohr
2559e312724SAndreas Gohr        // we only want positive results
2569e312724SAndreas Gohr        if (($op < 0) && ($num[0] < $num[1])) rsort($num);
2579e312724SAndreas Gohr
2589e312724SAndreas Gohr        // prepare result and task text
2599e312724SAndreas Gohr        $res = $num[0] + ($num[1] * $op);
2609bc1fab2SApostolos P. Tsompanopoulos        $task = $num[0] . (($op < 0) ? '-' : '+') . $num[1] . '= ';
2619e312724SAndreas Gohr
26209b1e97eSAndreas Gohr        return [$task, $res];
2639e312724SAndreas Gohr    }
26477e00bf9SAndreas Gohr
26509b1e97eSAndreas Gohr    // endregion
26609b1e97eSAndreas Gohr
26709b1e97eSAndreas Gohr    // region Output Builders
26809b1e97eSAndreas Gohr
26977e00bf9SAndreas Gohr    /**
27077e00bf9SAndreas Gohr     * Create a CAPTCHA image
271104ec268SAndreas Gohr     *
272104ec268SAndreas Gohr     * @param string $text the letters to display
27309b1e97eSAndreas Gohr     * @return string The image data
27477e00bf9SAndreas Gohr     */
27509b1e97eSAndreas Gohr    public function imageCaptcha($text)
27618622736SAndreas Gohr    {
27777e00bf9SAndreas Gohr        $w = $this->getConf('width');
27877e00bf9SAndreas Gohr        $h = $this->getConf('height');
27977e00bf9SAndreas Gohr
28009b1e97eSAndreas Gohr        $fonts = glob(__DIR__ . '/fonts/*.ttf');
281dc091fd0SAndreas Gohr
28277e00bf9SAndreas Gohr        // create a white image
28328c14643SAndreas Gohr        $img = imagecreatetruecolor($w, $h);
28428c14643SAndreas Gohr        $white = imagecolorallocate($img, 255, 255, 255);
28528c14643SAndreas Gohr        imagefill($img, 0, 0, $white);
28677e00bf9SAndreas Gohr
28777e00bf9SAndreas Gohr        // add some lines as background noise
28877e00bf9SAndreas Gohr        for ($i = 0; $i < 30; $i++) {
28909b1e97eSAndreas Gohr            $color = imagecolorallocate($img, random_int(100, 250), random_int(100, 250), random_int(100, 250));
29009b1e97eSAndreas Gohr            imageline($img, random_int(0, $w), random_int(0, $h), random_int(0, $w), random_int(0, $h), $color);
29177e00bf9SAndreas Gohr        }
29277e00bf9SAndreas Gohr
29377e00bf9SAndreas Gohr        // draw the letters
29428c14643SAndreas Gohr        $txtlen = strlen($text);
29528c14643SAndreas Gohr        for ($i = 0; $i < $txtlen; $i++) {
296dc091fd0SAndreas Gohr            $font = $fonts[array_rand($fonts)];
29709b1e97eSAndreas Gohr            $color = imagecolorallocate($img, random_int(0, 100), random_int(0, 100), random_int(0, 100));
29809b1e97eSAndreas Gohr            $size = random_int(floor($h / 1.8), floor($h * 0.7));
29909b1e97eSAndreas Gohr            $angle = random_int(-35, 35);
30077e00bf9SAndreas Gohr
30128c14643SAndreas Gohr            $x = ($w * 0.05) + $i * floor($w * 0.9 / $txtlen);
30277e00bf9SAndreas Gohr            $cheight = $size + ($size * 0.5);
30377e00bf9SAndreas Gohr            $y = floor($h / 2 + $cheight / 3.8);
30477e00bf9SAndreas Gohr
30577e00bf9SAndreas Gohr            imagettftext($img, $size, $angle, $x, $y, $color, $font, $text[$i]);
30677e00bf9SAndreas Gohr        }
30777e00bf9SAndreas Gohr
30809b1e97eSAndreas Gohr        ob_start();
30977e00bf9SAndreas Gohr        imagepng($img);
31009b1e97eSAndreas Gohr        $image = ob_get_clean();
31177e00bf9SAndreas Gohr        imagedestroy($img);
31209b1e97eSAndreas Gohr        return $image;
31308f248e4SAndreas Gohr    }
31408f248e4SAndreas Gohr
31508f248e4SAndreas Gohr    /**
31663609b6eSAndreas Gohr     * Generate an audio captcha
31763609b6eSAndreas Gohr     *
31863609b6eSAndreas Gohr     * @param string $text
31909b1e97eSAndreas Gohr     * @return string The joined wav files
32063609b6eSAndreas Gohr     */
32109b1e97eSAndreas Gohr    public function audioCaptcha($text)
32209b1e97eSAndreas Gohr    {
32363609b6eSAndreas Gohr        global $conf;
32463609b6eSAndreas Gohr
32563609b6eSAndreas Gohr        $lc = __DIR__ . '/lang/' . $conf['lang'] . '/audio/';
32663609b6eSAndreas Gohr        $en = __DIR__ . '/lang/en/audio/';
32763609b6eSAndreas Gohr
32863609b6eSAndreas Gohr        $wavs = [];
32963609b6eSAndreas Gohr
33063609b6eSAndreas Gohr        $text = strtolower($text);
33163609b6eSAndreas Gohr        $txtlen = strlen($text);
33263609b6eSAndreas Gohr        for ($i = 0; $i < $txtlen; $i++) {
33363609b6eSAndreas Gohr            $char = $text[$i];
33463609b6eSAndreas Gohr            $file = $lc . $char . '.wav';
33563609b6eSAndreas Gohr            if (!@file_exists($file)) $file = $en . $char . '.wav';
33663609b6eSAndreas Gohr            $wavs[] = $file;
33763609b6eSAndreas Gohr        }
33863609b6eSAndreas Gohr
33909b1e97eSAndreas Gohr        return $this->joinwavs($wavs);
34063609b6eSAndreas Gohr    }
34163609b6eSAndreas Gohr
34263609b6eSAndreas Gohr    /**
34309b1e97eSAndreas Gohr     * Create an SVG of the given text
34409b1e97eSAndreas Gohr     *
34509b1e97eSAndreas Gohr     * @param string $text
34609b1e97eSAndreas Gohr     * @return string
34709b1e97eSAndreas Gohr     */
34809b1e97eSAndreas Gohr    public function svgCaptcha($text)
34909b1e97eSAndreas Gohr    {
35009b1e97eSAndreas Gohr        require_once(__DIR__ . '/EasySVG.php');
35109b1e97eSAndreas Gohr
35209b1e97eSAndreas Gohr        $fonts = glob(__DIR__ . '/fonts/*.svg');
35309b1e97eSAndreas Gohr
35409b1e97eSAndreas Gohr        $x = 0; // where we start to draw
35509b1e97eSAndreas Gohr        $y = 100; // our max height
35609b1e97eSAndreas Gohr
35709b1e97eSAndreas Gohr        $svg = new EasySVG();
35809b1e97eSAndreas Gohr
35909b1e97eSAndreas Gohr        // draw the letters
36009b1e97eSAndreas Gohr        $txtlen = strlen($text);
36109b1e97eSAndreas Gohr        for ($i = 0; $i < $txtlen; $i++) {
36209b1e97eSAndreas Gohr            $char = $text[$i];
36309b1e97eSAndreas Gohr            $size = random_int($y / 2, $y - $y * 0.1); // 50-90%
36409b1e97eSAndreas Gohr            $svg->setFontSVG($fonts[array_rand($fonts)]);
36509b1e97eSAndreas Gohr
36609b1e97eSAndreas Gohr            $svg->setFontSize($size);
36709b1e97eSAndreas Gohr            $svg->setLetterSpacing(round(random_int(1, 4) / 10, 2)); // 0.1 - 0.4
36809b1e97eSAndreas Gohr            $svg->addText($char, $x, random_int(0, round($y - $size))); // random up and down
36909b1e97eSAndreas Gohr
37009b1e97eSAndreas Gohr            [$w] = $svg->textDimensions($char);
37109b1e97eSAndreas Gohr            $x += $w;
37209b1e97eSAndreas Gohr        }
37309b1e97eSAndreas Gohr
37409b1e97eSAndreas Gohr        $svg->addAttribute('width', $x . 'px');
37509b1e97eSAndreas Gohr        $svg->addAttribute('height', $y . 'px');
37609b1e97eSAndreas Gohr        $svg->addAttribute('viewbox', "0 0 $x $y");
37709b1e97eSAndreas Gohr        return $svg->asXML();
37809b1e97eSAndreas Gohr    }
37909b1e97eSAndreas Gohr
38009b1e97eSAndreas Gohr    // endregion
38109b1e97eSAndreas Gohr
38209b1e97eSAndreas Gohr    // region Utilities
38309b1e97eSAndreas Gohr
38409b1e97eSAndreas Gohr    /**
385f044313dSAndreas Gohr     * Encrypt the given string with the cookie salt
386f044313dSAndreas Gohr     *
387f044313dSAndreas Gohr     * @param string $data
388f044313dSAndreas Gohr     * @return string
389f044313dSAndreas Gohr     */
39018622736SAndreas Gohr    public function encrypt($data)
39118622736SAndreas Gohr    {
39209b1e97eSAndreas Gohr        $data = auth_encrypt($data, auth_cookiesalt());
393f044313dSAndreas Gohr        return base64_encode($data);
394f044313dSAndreas Gohr    }
395f044313dSAndreas Gohr
396f044313dSAndreas Gohr    /**
397f044313dSAndreas Gohr     * Decrypt the given string with the cookie salt
398f044313dSAndreas Gohr     *
399f044313dSAndreas Gohr     * @param string $data
400f044313dSAndreas Gohr     * @return string
401f044313dSAndreas Gohr     */
40218622736SAndreas Gohr    public function decrypt($data)
40318622736SAndreas Gohr    {
404f044313dSAndreas Gohr        $data = base64_decode($data);
40509870f99SPatrick Brown        if ($data === false || $data === '') return false;
406f044313dSAndreas Gohr
40709b1e97eSAndreas Gohr        return auth_decrypt($data, auth_cookiesalt());
408f044313dSAndreas Gohr    }
40909b1e97eSAndreas Gohr
41009b1e97eSAndreas Gohr    /**
41109b1e97eSAndreas Gohr     * Adds random space characters within the given text
41209b1e97eSAndreas Gohr     *
41309b1e97eSAndreas Gohr     * Keeps subsequent numbers without spaces (for math problem)
41409b1e97eSAndreas Gohr     *
41509b1e97eSAndreas Gohr     * @param $text
41609b1e97eSAndreas Gohr     * @return string
41709b1e97eSAndreas Gohr     */
41809b1e97eSAndreas Gohr    protected function obfuscateText($text)
41909b1e97eSAndreas Gohr    {
42009b1e97eSAndreas Gohr        $new = '';
42109b1e97eSAndreas Gohr
42209b1e97eSAndreas Gohr        $spaces = [
42309b1e97eSAndreas Gohr            "\r",
42409b1e97eSAndreas Gohr            "\n",
42509b1e97eSAndreas Gohr            "\r\n",
42609b1e97eSAndreas Gohr            ' ',
42709b1e97eSAndreas Gohr            "\xC2\xA0",
42809b1e97eSAndreas Gohr            // \u00A0    NO-BREAK SPACE
42909b1e97eSAndreas Gohr            "\xE2\x80\x80",
43009b1e97eSAndreas Gohr            // \u2000    EN QUAD
43109b1e97eSAndreas Gohr            "\xE2\x80\x81",
43209b1e97eSAndreas Gohr            // \u2001    EM QUAD
43309b1e97eSAndreas Gohr            "\xE2\x80\x82",
43409b1e97eSAndreas Gohr            // \u2002    EN SPACE
43509b1e97eSAndreas Gohr            //         "\xE2\x80\x83", // \u2003    EM SPACE
43609b1e97eSAndreas Gohr            "\xE2\x80\x84",
43709b1e97eSAndreas Gohr            // \u2004    THREE-PER-EM SPACE
43809b1e97eSAndreas Gohr            "\xE2\x80\x85",
43909b1e97eSAndreas Gohr            // \u2005    FOUR-PER-EM SPACE
44009b1e97eSAndreas Gohr            "\xE2\x80\x86",
44109b1e97eSAndreas Gohr            // \u2006    SIX-PER-EM SPACE
44209b1e97eSAndreas Gohr            "\xE2\x80\x87",
44309b1e97eSAndreas Gohr            // \u2007    FIGURE SPACE
44409b1e97eSAndreas Gohr            "\xE2\x80\x88",
44509b1e97eSAndreas Gohr            // \u2008    PUNCTUATION SPACE
44609b1e97eSAndreas Gohr            "\xE2\x80\x89",
44709b1e97eSAndreas Gohr            // \u2009    THIN SPACE
44809b1e97eSAndreas Gohr            "\xE2\x80\x8A",
44909b1e97eSAndreas Gohr            // \u200A    HAIR SPACE
45009b1e97eSAndreas Gohr            "\xE2\x80\xAF",
45109b1e97eSAndreas Gohr            // \u202F    NARROW NO-BREAK SPACE
45209b1e97eSAndreas Gohr            "\xE2\x81\x9F",
45309b1e97eSAndreas Gohr            // \u205F    MEDIUM MATHEMATICAL SPACE
45409b1e97eSAndreas Gohr            "\xE1\xA0\x8E\r\n",
45509b1e97eSAndreas Gohr            // \u180E    MONGOLIAN VOWEL SEPARATOR
45609b1e97eSAndreas Gohr            "\xE2\x80\x8B\r\n",
45709b1e97eSAndreas Gohr            // \u200B    ZERO WIDTH SPACE
45809b1e97eSAndreas Gohr            "\xEF\xBB\xBF\r\n",
45909b1e97eSAndreas Gohr        ];
46009b1e97eSAndreas Gohr
46109b1e97eSAndreas Gohr        $len = strlen($text);
46209b1e97eSAndreas Gohr        for ($i = 0; $i < $len - 1; $i++) {
46309b1e97eSAndreas Gohr            $new .= $text[$i];
46409b1e97eSAndreas Gohr
46509b1e97eSAndreas Gohr            if (!is_numeric($text[$i + 1])) {
46609b1e97eSAndreas Gohr                $new .= $spaces[array_rand($spaces)];
46709b1e97eSAndreas Gohr            }
46809b1e97eSAndreas Gohr        }
46909b1e97eSAndreas Gohr        $new .= $text[$len - 1];
47009b1e97eSAndreas Gohr        return $new;
471f044313dSAndreas Gohr    }
472969b14c4SAndreas Gohr
47363609b6eSAndreas Gohr
47463609b6eSAndreas Gohr    /**
47563609b6eSAndreas Gohr     * Join multiple wav files
47663609b6eSAndreas Gohr     *
47763609b6eSAndreas Gohr     * All wave files need to have the same format and need to be uncompressed.
47863609b6eSAndreas Gohr     * The headers of the last file will be used (with recalculated datasize
47963609b6eSAndreas Gohr     * of course)
48063609b6eSAndreas Gohr     *
48163609b6eSAndreas Gohr     * @link http://ccrma.stanford.edu/CCRMA/Courses/422/projects/WaveFormat/
48263609b6eSAndreas Gohr     * @link http://www.thescripts.com/forum/thread3770.html
48363609b6eSAndreas Gohr     */
48463609b6eSAndreas Gohr    protected function joinwavs($wavs)
48563609b6eSAndreas Gohr    {
48609b1e97eSAndreas Gohr        $fields = implode(
48709b1e97eSAndreas Gohr            '/',
48809b1e97eSAndreas Gohr            [
48963609b6eSAndreas Gohr                'H8ChunkID',
49063609b6eSAndreas Gohr                'VChunkSize',
49163609b6eSAndreas Gohr                'H8Format',
49263609b6eSAndreas Gohr                'H8Subchunk1ID',
49363609b6eSAndreas Gohr                'VSubchunk1Size',
49463609b6eSAndreas Gohr                'vAudioFormat',
49563609b6eSAndreas Gohr                'vNumChannels',
49663609b6eSAndreas Gohr                'VSampleRate',
49763609b6eSAndreas Gohr                'VByteRate',
49863609b6eSAndreas Gohr                'vBlockAlign',
49909b1e97eSAndreas Gohr                'vBitsPerSample'
50009b1e97eSAndreas Gohr            ]
50163609b6eSAndreas Gohr        );
50263609b6eSAndreas Gohr
50363609b6eSAndreas Gohr        $data = '';
50463609b6eSAndreas Gohr        foreach ($wavs as $wav) {
50563609b6eSAndreas Gohr            $fp = fopen($wav, 'rb');
50663609b6eSAndreas Gohr            $header = fread($fp, 36);
50763609b6eSAndreas Gohr            $info = unpack($fields, $header);
50863609b6eSAndreas Gohr
50963609b6eSAndreas Gohr            // read optional extra stuff
51063609b6eSAndreas Gohr            if ($info['Subchunk1Size'] > 16) {
51163609b6eSAndreas Gohr                $header .= fread($fp, ($info['Subchunk1Size'] - 16));
51263609b6eSAndreas Gohr            }
51363609b6eSAndreas Gohr
51463609b6eSAndreas Gohr            // read SubChunk2ID
51563609b6eSAndreas Gohr            $header .= fread($fp, 4);
51663609b6eSAndreas Gohr
51763609b6eSAndreas Gohr            // read Subchunk2Size
51863609b6eSAndreas Gohr            $size = unpack('vsize', fread($fp, 4));
51963609b6eSAndreas Gohr            $size = $size['size'];
52063609b6eSAndreas Gohr
52163609b6eSAndreas Gohr            // read data
52263609b6eSAndreas Gohr            $data .= fread($fp, $size);
52363609b6eSAndreas Gohr        }
52463609b6eSAndreas Gohr
52563609b6eSAndreas Gohr        return $header . pack('V', strlen($data)) . $data;
52663609b6eSAndreas Gohr    }
52763609b6eSAndreas Gohr
52809b1e97eSAndreas Gohr    // endregion
52977e00bf9SAndreas Gohr}
530