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) . '&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) . '&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) . '&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) . '&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