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