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 197*5697ecf8SAndreas Gohr $comb = (int) $rand ^ $ident; // combine both values 198*5697ecf8SAndreas 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 { 4141cd9cde7SAndreas Gohr $img = DOKU_BASE . 'lib/plugins/captcha/sound.png'; // FIXME use svg icon 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 4211cd9cde7SAndreas Gohr $icon = sprintf( 4221cd9cde7SAndreas Gohr '<img src="%s" width="16" height="16" alt="%s" /></a>', 4231cd9cde7SAndreas Gohr $img, 4241cd9cde7SAndreas Gohr $this->getLang('soundlink') 4251cd9cde7SAndreas Gohr ); 4261cd9cde7SAndreas Gohr 4271cd9cde7SAndreas Gohr return sprintf( 4281cd9cde7SAndreas Gohr '<a href="%s?%s" class="JSnocheck audiolink" title="%s">%s</a>', 4291cd9cde7SAndreas Gohr $url, 4301cd9cde7SAndreas Gohr $param, 4311cd9cde7SAndreas Gohr $this->getLang('soundlink'), 4321cd9cde7SAndreas Gohr $icon 4331cd9cde7SAndreas Gohr ); 4341cd9cde7SAndreas Gohr } 4351cd9cde7SAndreas Gohr 4361cd9cde7SAndreas Gohr /** 4371cd9cde7SAndreas Gohr * The HTML to show a figlet captcha 4381cd9cde7SAndreas Gohr * 4391cd9cde7SAndreas Gohr * @param string $code the code to display 4401cd9cde7SAndreas Gohr * @return string 4411cd9cde7SAndreas Gohr */ 4421cd9cde7SAndreas Gohr protected function htmlFiglet($code) 4431cd9cde7SAndreas Gohr { 4441cd9cde7SAndreas Gohr require_once(__DIR__ . '/figlet.php'); 4451cd9cde7SAndreas Gohr $figlet = new phpFiglet(); 4461cd9cde7SAndreas Gohr if ($figlet->loadfont(__DIR__ . '/figlet.flf')) { 4471cd9cde7SAndreas Gohr return '<pre>' . rtrim($figlet->fetch($code)) . '</pre>'; 4481cd9cde7SAndreas Gohr } else { 4491cd9cde7SAndreas Gohr msg('Failed to load figlet.flf font file. CAPTCHA broken', -1); 4501cd9cde7SAndreas Gohr } 4511cd9cde7SAndreas Gohr return 'FAIL'; 4521cd9cde7SAndreas Gohr } 4531cd9cde7SAndreas Gohr 45409b1e97eSAndreas Gohr // endregion 45509b1e97eSAndreas Gohr 45609b1e97eSAndreas Gohr // region Utilities 45709b1e97eSAndreas Gohr 45809b1e97eSAndreas Gohr /** 459f044313dSAndreas Gohr * Encrypt the given string with the cookie salt 460f044313dSAndreas Gohr * 461f044313dSAndreas Gohr * @param string $data 462f044313dSAndreas Gohr * @return string 463f044313dSAndreas Gohr */ 46418622736SAndreas Gohr public function encrypt($data) 46518622736SAndreas Gohr { 46609b1e97eSAndreas Gohr $data = auth_encrypt($data, auth_cookiesalt()); 467f044313dSAndreas Gohr return base64_encode($data); 468f044313dSAndreas Gohr } 469f044313dSAndreas Gohr 470f044313dSAndreas Gohr /** 471f044313dSAndreas Gohr * Decrypt the given string with the cookie salt 472f044313dSAndreas Gohr * 473f044313dSAndreas Gohr * @param string $data 474f044313dSAndreas Gohr * @return string 475f044313dSAndreas Gohr */ 47618622736SAndreas Gohr public function decrypt($data) 47718622736SAndreas Gohr { 478f044313dSAndreas Gohr $data = base64_decode($data); 47909870f99SPatrick Brown if ($data === false || $data === '') return false; 480f044313dSAndreas Gohr 48109b1e97eSAndreas Gohr return auth_decrypt($data, auth_cookiesalt()); 482f044313dSAndreas Gohr } 48309b1e97eSAndreas Gohr 48409b1e97eSAndreas Gohr /** 48509b1e97eSAndreas Gohr * Adds random space characters within the given text 48609b1e97eSAndreas Gohr * 48709b1e97eSAndreas Gohr * Keeps subsequent numbers without spaces (for math problem) 48809b1e97eSAndreas Gohr * 48909b1e97eSAndreas Gohr * @param $text 49009b1e97eSAndreas Gohr * @return string 49109b1e97eSAndreas Gohr */ 49209b1e97eSAndreas Gohr protected function obfuscateText($text) 49309b1e97eSAndreas Gohr { 49409b1e97eSAndreas Gohr $new = ''; 49509b1e97eSAndreas Gohr 49609b1e97eSAndreas Gohr $spaces = [ 49709b1e97eSAndreas Gohr "\r", 49809b1e97eSAndreas Gohr "\n", 49909b1e97eSAndreas Gohr "\r\n", 50009b1e97eSAndreas Gohr ' ', 50109b1e97eSAndreas Gohr "\xC2\xA0", 50209b1e97eSAndreas Gohr // \u00A0 NO-BREAK SPACE 50309b1e97eSAndreas Gohr "\xE2\x80\x80", 50409b1e97eSAndreas Gohr // \u2000 EN QUAD 50509b1e97eSAndreas Gohr "\xE2\x80\x81", 50609b1e97eSAndreas Gohr // \u2001 EM QUAD 50709b1e97eSAndreas Gohr "\xE2\x80\x82", 50809b1e97eSAndreas Gohr // \u2002 EN SPACE 50909b1e97eSAndreas Gohr // "\xE2\x80\x83", // \u2003 EM SPACE 51009b1e97eSAndreas Gohr "\xE2\x80\x84", 51109b1e97eSAndreas Gohr // \u2004 THREE-PER-EM SPACE 51209b1e97eSAndreas Gohr "\xE2\x80\x85", 51309b1e97eSAndreas Gohr // \u2005 FOUR-PER-EM SPACE 51409b1e97eSAndreas Gohr "\xE2\x80\x86", 51509b1e97eSAndreas Gohr // \u2006 SIX-PER-EM SPACE 51609b1e97eSAndreas Gohr "\xE2\x80\x87", 51709b1e97eSAndreas Gohr // \u2007 FIGURE SPACE 51809b1e97eSAndreas Gohr "\xE2\x80\x88", 51909b1e97eSAndreas Gohr // \u2008 PUNCTUATION SPACE 52009b1e97eSAndreas Gohr "\xE2\x80\x89", 52109b1e97eSAndreas Gohr // \u2009 THIN SPACE 52209b1e97eSAndreas Gohr "\xE2\x80\x8A", 52309b1e97eSAndreas Gohr // \u200A HAIR SPACE 52409b1e97eSAndreas Gohr "\xE2\x80\xAF", 52509b1e97eSAndreas Gohr // \u202F NARROW NO-BREAK SPACE 52609b1e97eSAndreas Gohr "\xE2\x81\x9F", 52709b1e97eSAndreas Gohr // \u205F MEDIUM MATHEMATICAL SPACE 52809b1e97eSAndreas Gohr "\xE1\xA0\x8E\r\n", 52909b1e97eSAndreas Gohr // \u180E MONGOLIAN VOWEL SEPARATOR 53009b1e97eSAndreas Gohr "\xE2\x80\x8B\r\n", 53109b1e97eSAndreas Gohr // \u200B ZERO WIDTH SPACE 53209b1e97eSAndreas Gohr "\xEF\xBB\xBF\r\n", 53309b1e97eSAndreas Gohr ]; 53409b1e97eSAndreas Gohr 53509b1e97eSAndreas Gohr $len = strlen($text); 53609b1e97eSAndreas Gohr for ($i = 0; $i < $len - 1; $i++) { 53709b1e97eSAndreas Gohr $new .= $text[$i]; 53809b1e97eSAndreas Gohr 53909b1e97eSAndreas Gohr if (!is_numeric($text[$i + 1])) { 54009b1e97eSAndreas Gohr $new .= $spaces[array_rand($spaces)]; 54109b1e97eSAndreas Gohr } 54209b1e97eSAndreas Gohr } 54309b1e97eSAndreas Gohr $new .= $text[$len - 1]; 54409b1e97eSAndreas Gohr return $new; 545f044313dSAndreas Gohr } 546969b14c4SAndreas Gohr 54763609b6eSAndreas Gohr 54863609b6eSAndreas Gohr /** 54963609b6eSAndreas Gohr * Join multiple wav files 55063609b6eSAndreas Gohr * 55163609b6eSAndreas Gohr * All wave files need to have the same format and need to be uncompressed. 55263609b6eSAndreas Gohr * The headers of the last file will be used (with recalculated datasize 55363609b6eSAndreas Gohr * of course) 55463609b6eSAndreas Gohr * 55563609b6eSAndreas Gohr * @link http://ccrma.stanford.edu/CCRMA/Courses/422/projects/WaveFormat/ 55663609b6eSAndreas Gohr * @link http://www.thescripts.com/forum/thread3770.html 55763609b6eSAndreas Gohr */ 55863609b6eSAndreas Gohr protected function joinwavs($wavs) 55963609b6eSAndreas Gohr { 56009b1e97eSAndreas Gohr $fields = implode( 56109b1e97eSAndreas Gohr '/', 56209b1e97eSAndreas Gohr [ 56363609b6eSAndreas Gohr 'H8ChunkID', 56463609b6eSAndreas Gohr 'VChunkSize', 56563609b6eSAndreas Gohr 'H8Format', 56663609b6eSAndreas Gohr 'H8Subchunk1ID', 56763609b6eSAndreas Gohr 'VSubchunk1Size', 56863609b6eSAndreas Gohr 'vAudioFormat', 56963609b6eSAndreas Gohr 'vNumChannels', 57063609b6eSAndreas Gohr 'VSampleRate', 57163609b6eSAndreas Gohr 'VByteRate', 57263609b6eSAndreas Gohr 'vBlockAlign', 57309b1e97eSAndreas Gohr 'vBitsPerSample' 57409b1e97eSAndreas Gohr ] 57563609b6eSAndreas Gohr ); 57663609b6eSAndreas Gohr 57763609b6eSAndreas Gohr $data = ''; 57863609b6eSAndreas Gohr foreach ($wavs as $wav) { 57963609b6eSAndreas Gohr $fp = fopen($wav, 'rb'); 58063609b6eSAndreas Gohr $header = fread($fp, 36); 58163609b6eSAndreas Gohr $info = unpack($fields, $header); 58263609b6eSAndreas Gohr 58363609b6eSAndreas Gohr // read optional extra stuff 58463609b6eSAndreas Gohr if ($info['Subchunk1Size'] > 16) { 58563609b6eSAndreas Gohr $header .= fread($fp, ($info['Subchunk1Size'] - 16)); 58663609b6eSAndreas Gohr } 58763609b6eSAndreas Gohr 58863609b6eSAndreas Gohr // read SubChunk2ID 58963609b6eSAndreas Gohr $header .= fread($fp, 4); 59063609b6eSAndreas Gohr 59163609b6eSAndreas Gohr // read Subchunk2Size 59263609b6eSAndreas Gohr $size = unpack('vsize', fread($fp, 4)); 59363609b6eSAndreas Gohr $size = $size['size']; 59463609b6eSAndreas Gohr 59563609b6eSAndreas Gohr // read data 59663609b6eSAndreas Gohr $data .= fread($fp, $size); 59763609b6eSAndreas Gohr } 59863609b6eSAndreas Gohr 59963609b6eSAndreas Gohr return $header . pack('V', strlen($data)) . $data; 60063609b6eSAndreas Gohr } 60163609b6eSAndreas Gohr 60209b1e97eSAndreas Gohr // endregion 60377e00bf9SAndreas Gohr} 604