177e00bf9SAndreas Gohr<?php 2*09b1e97eSAndreas Gohr 3*09b1e97eSAndreas Gohruse dokuwiki\Extension\Plugin; 4*09b1e97eSAndreas Gohruse dokuwiki\Utf8\PhpString; 5*09b1e97eSAndreas Gohr 677e00bf9SAndreas Gohr/** 777e00bf9SAndreas Gohr * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 877e00bf9SAndreas Gohr * @author Andreas Gohr <andi@splitbrain.org> 977e00bf9SAndreas Gohr */ 107218f96cSAndreas Gohr 11a285df67SAndreas Gohr/** 12a285df67SAndreas Gohr * Class helper_plugin_captcha 13a285df67SAndreas Gohr */ 14*09b1e97eSAndreas Gohrclass helper_plugin_captcha extends Plugin 1518622736SAndreas Gohr{ 1623d379a8SAndreas Gohr protected $field_in = 'plugin__captcha'; 1723d379a8SAndreas Gohr protected $field_sec = 'plugin__captcha_secret'; 1823d379a8SAndreas Gohr protected $field_hp = 'plugin__captcha_honeypot'; 1923d379a8SAndreas Gohr 20*09b1e97eSAndreas Gohr // region Public API 21*09b1e97eSAndreas Gohr 2223d379a8SAndreas Gohr /** 2323d379a8SAndreas Gohr * Constructor. Initializes field names 2423d379a8SAndreas Gohr */ 2518622736SAndreas Gohr public function __construct() 2618622736SAndreas Gohr { 27*09b1e97eSAndreas Gohr $this->field_in = md5($this->fixedIdent() . $this->field_in); 28*09b1e97eSAndreas Gohr $this->field_sec = md5($this->fixedIdent() . $this->field_sec); 29*09b1e97eSAndreas Gohr $this->field_hp = md5($this->fixedIdent() . $this->field_hp); 3023d379a8SAndreas Gohr } 3123d379a8SAndreas Gohr 3277e00bf9SAndreas Gohr /** 3377e00bf9SAndreas Gohr * Check if the CAPTCHA should be used. Always check this before using the methods below. 3477e00bf9SAndreas Gohr * 3577e00bf9SAndreas Gohr * @return bool true when the CAPTCHA should be used 3677e00bf9SAndreas Gohr */ 3718622736SAndreas Gohr public function isEnabled() 3818622736SAndreas Gohr { 3964382f29SAndreas Gohr global $INPUT; 4064382f29SAndreas Gohr if (!$this->getConf('forusers') && $INPUT->server->str('REMOTE_USER')) return false; 4177e00bf9SAndreas Gohr return true; 4277e00bf9SAndreas Gohr } 4377e00bf9SAndreas Gohr 4477e00bf9SAndreas Gohr /** 4577e00bf9SAndreas Gohr * Returns the HTML to display the CAPTCHA with the chosen method 46*09b1e97eSAndreas Gohr * 47*09b1e97eSAndreas Gohr * @return string The HTML to display the CAPTCHA 4877e00bf9SAndreas Gohr */ 4918622736SAndreas Gohr public function getHTML() 5018622736SAndreas Gohr { 5177e00bf9SAndreas Gohr global $ID; 5277e00bf9SAndreas Gohr 53*09b1e97eSAndreas Gohr $rand = (float)(random_int(0, 10000)) / 10000; 54*09b1e97eSAndreas Gohr $this->storeCaptchaCookie($this->fixedIdent(), $rand); 55a285df67SAndreas Gohr 569e312724SAndreas Gohr if ($this->getConf('mode') == 'math') { 57*09b1e97eSAndreas Gohr $code = $this->generateMath($this->fixedIdent(), $rand); 589e312724SAndreas Gohr $code = $code[0]; 599e312724SAndreas Gohr $text = $this->getLang('fillmath'); 60df8afac4SAndreas Gohr } elseif ($this->getConf('mode') == 'question') { 61a285df67SAndreas Gohr $code = ''; // not used 62df8afac4SAndreas Gohr $text = $this->getConf('question'); 639e312724SAndreas Gohr } else { 64*09b1e97eSAndreas Gohr $code = $this->generateCaptchaCode($this->fixedIdent(), $rand); 659e312724SAndreas Gohr $text = $this->getLang('fillcaptcha'); 669e312724SAndreas Gohr } 67f044313dSAndreas Gohr $secret = $this->encrypt($rand); 6877e00bf9SAndreas Gohr 6928c14643SAndreas Gohr $txtlen = $this->getConf('lettercount'); 7028c14643SAndreas Gohr 7177e00bf9SAndreas Gohr $out = ''; 7277e00bf9SAndreas Gohr $out .= '<div id="plugin__captcha_wrapper">'; 7323d379a8SAndreas Gohr $out .= '<input type="hidden" name="' . $this->field_sec . '" value="' . hsc($secret) . '" />'; 749e312724SAndreas Gohr $out .= '<label for="plugin__captcha">' . $text . '</label> '; 7523d379a8SAndreas Gohr 7677e00bf9SAndreas Gohr switch ($this->getConf('mode')) { 779e312724SAndreas Gohr case 'math': 789d6f09afSAndreas Gohr case 'text': 79*09b1e97eSAndreas Gohr $out .= $this->obfuscateText($code); 8077e00bf9SAndreas Gohr break; 8177e00bf9SAndreas Gohr case 'js': 82*09b1e97eSAndreas Gohr $out .= '<span id="plugin__captcha_code">' . $this->obfuscateText($code) . '</span>'; 8377e00bf9SAndreas Gohr break; 8408f248e4SAndreas Gohr case 'svg': 8508f248e4SAndreas Gohr $out .= '<span class="svg" style="width:' . $this->getConf('width') . 'px; height:' . $this->getConf('height') . 'px">'; 86*09b1e97eSAndreas Gohr $out .= $this->svgCaptcha($code); 8708f248e4SAndreas Gohr $out .= '</span>'; 8808f248e4SAndreas Gohr break; 8908f248e4SAndreas Gohr case 'svgaudio': 9008f248e4SAndreas Gohr $out .= '<span class="svg" style="width:' . $this->getConf('width') . 'px; height:' . $this->getConf('height') . 'px">'; 91*09b1e97eSAndreas Gohr $out .= $this->svgCaptcha($code); 9208f248e4SAndreas Gohr $out .= '</span>'; 9308f248e4SAndreas Gohr $out .= '<a href="' . DOKU_BASE . 'lib/plugins/captcha/wav.php?secret=' . rawurlencode($secret) . '&id=' . $ID . '"' . 949efb703bSStefan Bethke ' class="JSnocheck audiolink" title="' . $this->getLang('soundlink') . '">'; 9508f248e4SAndreas Gohr $out .= '<img src="' . DOKU_BASE . 'lib/plugins/captcha/sound.png" width="16" height="16"' . 9608f248e4SAndreas Gohr ' alt="' . $this->getLang('soundlink') . '" /></a>'; 9708f248e4SAndreas Gohr break; 9877e00bf9SAndreas Gohr case 'image': 9977e00bf9SAndreas Gohr $out .= '<img src="' . DOKU_BASE . 'lib/plugins/captcha/img.php?secret=' . rawurlencode($secret) . '&id=' . $ID . '" ' . 10077e00bf9SAndreas Gohr ' width="' . $this->getConf('width') . '" height="' . $this->getConf('height') . '" alt="" /> '; 10177e00bf9SAndreas Gohr break; 10277e00bf9SAndreas Gohr case 'audio': 10377e00bf9SAndreas Gohr $out .= '<img src="' . DOKU_BASE . 'lib/plugins/captcha/img.php?secret=' . rawurlencode($secret) . '&id=' . $ID . '" ' . 10477e00bf9SAndreas Gohr ' width="' . $this->getConf('width') . '" height="' . $this->getConf('height') . '" alt="" /> '; 10577e00bf9SAndreas Gohr $out .= '<a href="' . DOKU_BASE . 'lib/plugins/captcha/wav.php?secret=' . rawurlencode($secret) . '&id=' . $ID . '"' . 1069efb703bSStefan Bethke ' class="JSnocheck audiolink" title="' . $this->getLang('soundlink') . '">'; 10777e00bf9SAndreas Gohr $out .= '<img src="' . DOKU_BASE . 'lib/plugins/captcha/sound.png" width="16" height="16"' . 10877e00bf9SAndreas Gohr ' alt="' . $this->getLang('soundlink') . '" /></a>'; 10977e00bf9SAndreas Gohr break; 11052e95008SAndreas Gohr case 'figlet': 111*09b1e97eSAndreas Gohr require_once(__DIR__ . '/figlet.php'); 11252e95008SAndreas Gohr $figlet = new phpFiglet(); 113*09b1e97eSAndreas Gohr if ($figlet->loadfont(__DIR__ . '/figlet.flf')) { 11452e95008SAndreas Gohr $out .= '<pre>'; 11552e95008SAndreas Gohr $out .= rtrim($figlet->fetch($code)); 11652e95008SAndreas Gohr $out .= '</pre>'; 11752e95008SAndreas Gohr } else { 11852e95008SAndreas Gohr msg('Failed to load figlet.flf font file. CAPTCHA broken', -1); 11952e95008SAndreas Gohr } 12052e95008SAndreas Gohr break; 12177e00bf9SAndreas Gohr } 122df8afac4SAndreas Gohr $out .= ' <input type="text" size="' . $txtlen . '" name="' . $this->field_in . '" class="edit" /> '; 12323d379a8SAndreas Gohr 12423d379a8SAndreas Gohr // add honeypot field 1259d63c05fSlainme $out .= '<label class="no">' . $this->getLang('honeypot') . '<input type="text" name="' . $this->field_hp . '" /></label>'; 12677e00bf9SAndreas Gohr $out .= '</div>'; 12777e00bf9SAndreas Gohr return $out; 12877e00bf9SAndreas Gohr } 12977e00bf9SAndreas Gohr 13077e00bf9SAndreas Gohr /** 13118622736SAndreas Gohr * Checks if the CAPTCHA was solved correctly 13277e00bf9SAndreas Gohr * 13377e00bf9SAndreas Gohr * @param bool $msg when true, an error will be signalled through the msg() method 13477e00bf9SAndreas Gohr * @return bool true when the answer was correct, otherwise false 13577e00bf9SAndreas Gohr */ 13618622736SAndreas Gohr public function check($msg = true) 13718622736SAndreas Gohr { 138478e363cSAndreas Gohr global $INPUT; 139478e363cSAndreas Gohr 140478e363cSAndreas Gohr $field_sec = $INPUT->str($this->field_sec); 141478e363cSAndreas Gohr $field_in = $INPUT->str($this->field_in); 142478e363cSAndreas Gohr $field_hp = $INPUT->str($this->field_hp); 143478e363cSAndreas Gohr 144478e363cSAndreas Gohr // reconstruct captcha from provided $field_sec 145478e363cSAndreas Gohr $rand = $this->decrypt($field_sec); 1469e312724SAndreas Gohr 1479e312724SAndreas Gohr if ($this->getConf('mode') == 'math') { 148*09b1e97eSAndreas Gohr $code = $this->generateMath($this->fixedIdent(), $rand); 1499e312724SAndreas Gohr $code = $code[1]; 150df8afac4SAndreas Gohr } elseif ($this->getConf('mode') == 'question') { 151df8afac4SAndreas Gohr $code = $this->getConf('answer'); 1529e312724SAndreas Gohr } else { 153*09b1e97eSAndreas Gohr $code = $this->generateCaptchaCode($this->fixedIdent(), $rand); 1549e312724SAndreas Gohr } 15577e00bf9SAndreas Gohr 156478e363cSAndreas Gohr // compare values 157*09b1e97eSAndreas Gohr if ( 158*09b1e97eSAndreas Gohr !$field_sec || 159478e363cSAndreas Gohr !$field_in || 16014e271ebSPatrick Brown $rand === false || 161*09b1e97eSAndreas Gohr PhpString::strtolower($field_in) != PhpString::strtolower($code) || 162a285df67SAndreas Gohr trim($field_hp) !== '' || 163*09b1e97eSAndreas Gohr !$this->retrieveCaptchaCookie($this->fixedIdent(), $rand) 16423d379a8SAndreas Gohr ) { 16577e00bf9SAndreas Gohr if ($msg) msg($this->getLang('testfailed'), -1); 16677e00bf9SAndreas Gohr return false; 16777e00bf9SAndreas Gohr } 16877e00bf9SAndreas Gohr return true; 16977e00bf9SAndreas Gohr } 17077e00bf9SAndreas Gohr 171*09b1e97eSAndreas Gohr // endregion 172*09b1e97eSAndreas Gohr 173*09b1e97eSAndreas Gohr // region Captcha Cookie methods 174*09b1e97eSAndreas Gohr 17577e00bf9SAndreas Gohr /** 176cde3ece1SAndreas Gohr * Get the path where a captcha cookie would be stored 177a285df67SAndreas Gohr * 178a285df67SAndreas Gohr * We use a daily temp directory which is easy to clean up 179a285df67SAndreas Gohr * 180a285df67SAndreas Gohr * @param $fixed string the fixed part, any string 181a285df67SAndreas Gohr * @param $rand float some random number between 0 and 1 182a285df67SAndreas Gohr * @return string the path to the cookie file 183a285df67SAndreas Gohr */ 18418622736SAndreas Gohr protected function getCaptchaCookiePath($fixed, $rand) 18518622736SAndreas Gohr { 186a285df67SAndreas Gohr global $conf; 187a285df67SAndreas Gohr $path = $conf['tmpdir'] . '/captcha/' . date('Y-m-d') . '/' . md5($fixed . $rand) . '.cookie'; 188a285df67SAndreas Gohr io_makeFileDir($path); 189a285df67SAndreas Gohr return $path; 190a285df67SAndreas Gohr } 191a285df67SAndreas Gohr 192a285df67SAndreas Gohr /** 193cde3ece1SAndreas Gohr * remove all outdated captcha cookies 194cde3ece1SAndreas Gohr */ 195*09b1e97eSAndreas Gohr public function cleanCaptchaCookies() 19618622736SAndreas Gohr { 197cde3ece1SAndreas Gohr global $conf; 198cde3ece1SAndreas Gohr $path = $conf['tmpdir'] . '/captcha/'; 199cde3ece1SAndreas Gohr $dirs = glob("$path/*", GLOB_ONLYDIR); 200cde3ece1SAndreas Gohr $today = date('Y-m-d'); 201cde3ece1SAndreas Gohr foreach ($dirs as $dir) { 202cde3ece1SAndreas Gohr if (basename($dir) === $today) continue; 203cde3ece1SAndreas Gohr if (!preg_match('/\/captcha\//', $dir)) continue; // safety net 204cde3ece1SAndreas Gohr io_rmdir($dir, true); 205cde3ece1SAndreas Gohr } 206cde3ece1SAndreas Gohr } 207cde3ece1SAndreas Gohr 208cde3ece1SAndreas Gohr /** 209a285df67SAndreas Gohr * Creates a one time captcha cookie 210a285df67SAndreas Gohr * 211a285df67SAndreas Gohr * This is used to prevent replay attacks. It is generated when the captcha form 212a285df67SAndreas Gohr * is shown and checked with the captcha check. Since we can not be sure about the 213a285df67SAndreas Gohr * session state (might be closed or open) we're not using it. 214a285df67SAndreas Gohr * 215a285df67SAndreas Gohr * We're not using the stored values for displaying the captcha image (or audio) 216a285df67SAndreas Gohr * but continue to use our encryption scheme. This way it's still possible to have 217a285df67SAndreas Gohr * multiple captcha checks going on in parallel (eg. with multiple browser tabs) 218a285df67SAndreas Gohr * 219a285df67SAndreas Gohr * @param $fixed string the fixed part, any string 220a285df67SAndreas Gohr * @param $rand float some random number between 0 and 1 221a285df67SAndreas Gohr */ 22218622736SAndreas Gohr protected function storeCaptchaCookie($fixed, $rand) 22318622736SAndreas Gohr { 224a285df67SAndreas Gohr $cache = $this->getCaptchaCookiePath($fixed, $rand); 225a285df67SAndreas Gohr touch($cache); 226a285df67SAndreas Gohr } 227a285df67SAndreas Gohr 228a285df67SAndreas Gohr /** 229a285df67SAndreas Gohr * Checks if the captcha cookie exists and deletes it 230a285df67SAndreas Gohr * 231a285df67SAndreas Gohr * @param $fixed string the fixed part, any string 232a285df67SAndreas Gohr * @param $rand float some random number between 0 and 1 233a285df67SAndreas Gohr * @return bool true if the cookie existed 234a285df67SAndreas Gohr */ 23518622736SAndreas Gohr protected function retrieveCaptchaCookie($fixed, $rand) 23618622736SAndreas Gohr { 237a285df67SAndreas Gohr $cache = $this->getCaptchaCookiePath($fixed, $rand); 238a285df67SAndreas Gohr if (file_exists($cache)) { 239a285df67SAndreas Gohr unlink($cache); 240a285df67SAndreas Gohr return true; 241a285df67SAndreas Gohr } 242a285df67SAndreas Gohr return false; 243a285df67SAndreas Gohr } 244a285df67SAndreas Gohr 245*09b1e97eSAndreas Gohr // endregion 246*09b1e97eSAndreas Gohr 247*09b1e97eSAndreas Gohr // region Captcha Generation methods 248*09b1e97eSAndreas Gohr 249a285df67SAndreas Gohr /** 25077e00bf9SAndreas Gohr * Build a semi-secret fixed string identifying the current page and user 25177e00bf9SAndreas Gohr * 25277e00bf9SAndreas Gohr * This string is always the same for the current user when editing the same 25327d84d8dSAndreas Gohr * page revision, but only for one day. Editing a page before midnight and saving 25427d84d8dSAndreas Gohr * after midnight will result in a failed CAPTCHA once, but makes sure it can 25527d84d8dSAndreas Gohr * not be reused which is especially important for the registration form where the 25627d84d8dSAndreas Gohr * $ID usually won't change. 257104ec268SAndreas Gohr * 258104ec268SAndreas Gohr * @return string 25977e00bf9SAndreas Gohr */ 260*09b1e97eSAndreas Gohr public function fixedIdent() 26118622736SAndreas Gohr { 26277e00bf9SAndreas Gohr global $ID; 26377e00bf9SAndreas Gohr $lm = @filemtime(wikiFN($ID)); 26427d84d8dSAndreas Gohr $td = date('Y-m-d'); 26563609b6eSAndreas Gohr $ip = clientIP(); 26663609b6eSAndreas Gohr $salt = auth_cookiesalt(); 26763609b6eSAndreas Gohr 268*09b1e97eSAndreas Gohr return sha1(implode("\n", [$ID, $lm, $td, $ip, $salt])); 26977e00bf9SAndreas Gohr } 27077e00bf9SAndreas Gohr 27177e00bf9SAndreas Gohr /** 272*09b1e97eSAndreas Gohr * Generate a magic code based on the given data 2739d6f09afSAndreas Gohr * 274*09b1e97eSAndreas Gohr * This "magic" code represents the given fixed identifier (see fixedIdent()) and the given 275*09b1e97eSAndreas Gohr * random number. It is used to generate the actual CAPTCHA code. 2769d6f09afSAndreas Gohr * 277*09b1e97eSAndreas Gohr * @param $ident string the fixed part, any string 278a02b2219SPatrick Brown * @param $rand float some random number between 0 and 1 279a02b2219SPatrick Brown * @return string 280a02b2219SPatrick Brown */ 281*09b1e97eSAndreas Gohr protected function generateMagicCode($ident, $rand) 28218622736SAndreas Gohr { 283*09b1e97eSAndreas Gohr $ident = hexdec(substr(md5($ident), 5, 5)); // use part of the md5 to generate an int 284*09b1e97eSAndreas Gohr $rand *= 0xFFFFF; // bitmask from the random number 285*09b1e97eSAndreas Gohr return md5($rand ^ $ident); // combine both values 286a02b2219SPatrick Brown } 287a02b2219SPatrick Brown 288a02b2219SPatrick Brown /** 289*09b1e97eSAndreas Gohr * Generates a char string based on the given data 29077e00bf9SAndreas Gohr * 291*09b1e97eSAndreas Gohr * The string is pseudo random based on a fixed identifier (see fixedIdent()) and a random number. 292*09b1e97eSAndreas Gohr * 293*09b1e97eSAndreas Gohr * @param $ident string the fixed part, any string 294104ec268SAndreas Gohr * @param $rand float some random number between 0 and 1 29523d379a8SAndreas Gohr * @return string 29677e00bf9SAndreas Gohr */ 297*09b1e97eSAndreas Gohr public function generateCaptchaCode($ident, $rand) 29818622736SAndreas Gohr { 299*09b1e97eSAndreas Gohr $numbers = $this->generateMagicCode($ident, $rand); 30077e00bf9SAndreas Gohr 30177e00bf9SAndreas Gohr // now create the letters 30277e00bf9SAndreas Gohr $code = ''; 3039a516edaSPatrick Brown $lettercount = $this->getConf('lettercount') * 2; 3049a516edaSPatrick Brown if ($lettercount > strlen($numbers)) $lettercount = strlen($numbers); 3059a516edaSPatrick Brown for ($i = 0; $i < $lettercount; $i += 2) { 30677e00bf9SAndreas Gohr $code .= chr(floor(hexdec($numbers[$i] . $numbers[$i + 1]) / 10) + 65); 30777e00bf9SAndreas Gohr } 30877e00bf9SAndreas Gohr 30977e00bf9SAndreas Gohr return $code; 31077e00bf9SAndreas Gohr } 31177e00bf9SAndreas Gohr 3129e312724SAndreas Gohr /** 3139e312724SAndreas Gohr * Create a mathematical task and its result 3149e312724SAndreas Gohr * 315*09b1e97eSAndreas Gohr * @param $ident string the fixed part, any string 316104ec268SAndreas Gohr * @param $rand float some random number between 0 and 1 317*09b1e97eSAndreas Gohr * @return array [task, result] 3189e312724SAndreas Gohr */ 319*09b1e97eSAndreas Gohr protected function generateMath($ident, $rand) 32018622736SAndreas Gohr { 321*09b1e97eSAndreas Gohr $numbers = $this->generateMagicCode($ident, $rand); 3229e312724SAndreas Gohr 3239e312724SAndreas Gohr // first letter is the operator (+/-) 3249e312724SAndreas Gohr $op = (hexdec($numbers[0]) > 8) ? -1 : 1; 325*09b1e97eSAndreas Gohr $num = [hexdec($numbers[1] . $numbers[2]), hexdec($numbers[3])]; 3269e312724SAndreas Gohr 3279e312724SAndreas Gohr // we only want positive results 3289e312724SAndreas Gohr if (($op < 0) && ($num[0] < $num[1])) rsort($num); 3299e312724SAndreas Gohr 3309e312724SAndreas Gohr // prepare result and task text 3319e312724SAndreas Gohr $res = $num[0] + ($num[1] * $op); 3329bc1fab2SApostolos P. Tsompanopoulos $task = $num[0] . (($op < 0) ? '-' : '+') . $num[1] . '= '; 3339e312724SAndreas Gohr 334*09b1e97eSAndreas Gohr return [$task, $res]; 3359e312724SAndreas Gohr } 33677e00bf9SAndreas Gohr 337*09b1e97eSAndreas Gohr // endregion 338*09b1e97eSAndreas Gohr 339*09b1e97eSAndreas Gohr // region Output Builders 340*09b1e97eSAndreas Gohr 34177e00bf9SAndreas Gohr /** 34277e00bf9SAndreas Gohr * Create a CAPTCHA image 343104ec268SAndreas Gohr * 344104ec268SAndreas Gohr * @param string $text the letters to display 345*09b1e97eSAndreas Gohr * @return string The image data 34677e00bf9SAndreas Gohr */ 347*09b1e97eSAndreas Gohr public function imageCaptcha($text) 34818622736SAndreas Gohr { 34977e00bf9SAndreas Gohr $w = $this->getConf('width'); 35077e00bf9SAndreas Gohr $h = $this->getConf('height'); 35177e00bf9SAndreas Gohr 352*09b1e97eSAndreas Gohr $fonts = glob(__DIR__ . '/fonts/*.ttf'); 353dc091fd0SAndreas Gohr 35477e00bf9SAndreas Gohr // create a white image 35528c14643SAndreas Gohr $img = imagecreatetruecolor($w, $h); 35628c14643SAndreas Gohr $white = imagecolorallocate($img, 255, 255, 255); 35728c14643SAndreas Gohr imagefill($img, 0, 0, $white); 35877e00bf9SAndreas Gohr 35977e00bf9SAndreas Gohr // add some lines as background noise 36077e00bf9SAndreas Gohr for ($i = 0; $i < 30; $i++) { 361*09b1e97eSAndreas Gohr $color = imagecolorallocate($img, random_int(100, 250), random_int(100, 250), random_int(100, 250)); 362*09b1e97eSAndreas Gohr imageline($img, random_int(0, $w), random_int(0, $h), random_int(0, $w), random_int(0, $h), $color); 36377e00bf9SAndreas Gohr } 36477e00bf9SAndreas Gohr 36577e00bf9SAndreas Gohr // draw the letters 36628c14643SAndreas Gohr $txtlen = strlen($text); 36728c14643SAndreas Gohr for ($i = 0; $i < $txtlen; $i++) { 368dc091fd0SAndreas Gohr $font = $fonts[array_rand($fonts)]; 369*09b1e97eSAndreas Gohr $color = imagecolorallocate($img, random_int(0, 100), random_int(0, 100), random_int(0, 100)); 370*09b1e97eSAndreas Gohr $size = random_int(floor($h / 1.8), floor($h * 0.7)); 371*09b1e97eSAndreas Gohr $angle = random_int(-35, 35); 37277e00bf9SAndreas Gohr 37328c14643SAndreas Gohr $x = ($w * 0.05) + $i * floor($w * 0.9 / $txtlen); 37477e00bf9SAndreas Gohr $cheight = $size + ($size * 0.5); 37577e00bf9SAndreas Gohr $y = floor($h / 2 + $cheight / 3.8); 37677e00bf9SAndreas Gohr 37777e00bf9SAndreas Gohr imagettftext($img, $size, $angle, $x, $y, $color, $font, $text[$i]); 37877e00bf9SAndreas Gohr } 37977e00bf9SAndreas Gohr 380*09b1e97eSAndreas Gohr ob_start(); 38177e00bf9SAndreas Gohr imagepng($img); 382*09b1e97eSAndreas Gohr $image = ob_get_clean(); 38377e00bf9SAndreas Gohr imagedestroy($img); 384*09b1e97eSAndreas Gohr return $image; 38508f248e4SAndreas Gohr } 38608f248e4SAndreas Gohr 38708f248e4SAndreas Gohr /** 38863609b6eSAndreas Gohr * Generate an audio captcha 38963609b6eSAndreas Gohr * 39063609b6eSAndreas Gohr * @param string $text 391*09b1e97eSAndreas Gohr * @return string The joined wav files 39263609b6eSAndreas Gohr */ 393*09b1e97eSAndreas Gohr public function audioCaptcha($text) 394*09b1e97eSAndreas Gohr { 39563609b6eSAndreas Gohr global $conf; 39663609b6eSAndreas Gohr 39763609b6eSAndreas Gohr $lc = __DIR__ . '/lang/' . $conf['lang'] . '/audio/'; 39863609b6eSAndreas Gohr $en = __DIR__ . '/lang/en/audio/'; 39963609b6eSAndreas Gohr 40063609b6eSAndreas Gohr $wavs = []; 40163609b6eSAndreas Gohr 40263609b6eSAndreas Gohr $text = strtolower($text); 40363609b6eSAndreas Gohr $txtlen = strlen($text); 40463609b6eSAndreas Gohr for ($i = 0; $i < $txtlen; $i++) { 40563609b6eSAndreas Gohr $char = $text[$i]; 40663609b6eSAndreas Gohr $file = $lc . $char . '.wav'; 40763609b6eSAndreas Gohr if (!@file_exists($file)) $file = $en . $char . '.wav'; 40863609b6eSAndreas Gohr $wavs[] = $file; 40963609b6eSAndreas Gohr } 41063609b6eSAndreas Gohr 411*09b1e97eSAndreas Gohr return $this->joinwavs($wavs); 41263609b6eSAndreas Gohr } 41363609b6eSAndreas Gohr 41463609b6eSAndreas Gohr /** 415*09b1e97eSAndreas Gohr * Create an SVG of the given text 416*09b1e97eSAndreas Gohr * 417*09b1e97eSAndreas Gohr * @param string $text 418*09b1e97eSAndreas Gohr * @return string 419*09b1e97eSAndreas Gohr */ 420*09b1e97eSAndreas Gohr public function svgCaptcha($text) 421*09b1e97eSAndreas Gohr { 422*09b1e97eSAndreas Gohr require_once(__DIR__ . '/EasySVG.php'); 423*09b1e97eSAndreas Gohr 424*09b1e97eSAndreas Gohr $fonts = glob(__DIR__ . '/fonts/*.svg'); 425*09b1e97eSAndreas Gohr 426*09b1e97eSAndreas Gohr $x = 0; // where we start to draw 427*09b1e97eSAndreas Gohr $y = 100; // our max height 428*09b1e97eSAndreas Gohr 429*09b1e97eSAndreas Gohr $svg = new EasySVG(); 430*09b1e97eSAndreas Gohr 431*09b1e97eSAndreas Gohr // draw the letters 432*09b1e97eSAndreas Gohr $txtlen = strlen($text); 433*09b1e97eSAndreas Gohr for ($i = 0; $i < $txtlen; $i++) { 434*09b1e97eSAndreas Gohr $char = $text[$i]; 435*09b1e97eSAndreas Gohr $size = random_int($y / 2, $y - $y * 0.1); // 50-90% 436*09b1e97eSAndreas Gohr $svg->setFontSVG($fonts[array_rand($fonts)]); 437*09b1e97eSAndreas Gohr 438*09b1e97eSAndreas Gohr $svg->setFontSize($size); 439*09b1e97eSAndreas Gohr $svg->setLetterSpacing(round(random_int(1, 4) / 10, 2)); // 0.1 - 0.4 440*09b1e97eSAndreas Gohr $svg->addText($char, $x, random_int(0, round($y - $size))); // random up and down 441*09b1e97eSAndreas Gohr 442*09b1e97eSAndreas Gohr [$w] = $svg->textDimensions($char); 443*09b1e97eSAndreas Gohr $x += $w; 444*09b1e97eSAndreas Gohr } 445*09b1e97eSAndreas Gohr 446*09b1e97eSAndreas Gohr $svg->addAttribute('width', $x . 'px'); 447*09b1e97eSAndreas Gohr $svg->addAttribute('height', $y . 'px'); 448*09b1e97eSAndreas Gohr $svg->addAttribute('viewbox', "0 0 $x $y"); 449*09b1e97eSAndreas Gohr return $svg->asXML(); 450*09b1e97eSAndreas Gohr } 451*09b1e97eSAndreas Gohr 452*09b1e97eSAndreas Gohr // endregion 453*09b1e97eSAndreas Gohr 454*09b1e97eSAndreas Gohr // region Utilities 455*09b1e97eSAndreas Gohr 456*09b1e97eSAndreas Gohr /** 457f044313dSAndreas Gohr * Encrypt the given string with the cookie salt 458f044313dSAndreas Gohr * 459f044313dSAndreas Gohr * @param string $data 460f044313dSAndreas Gohr * @return string 461f044313dSAndreas Gohr */ 46218622736SAndreas Gohr public function encrypt($data) 46318622736SAndreas Gohr { 464*09b1e97eSAndreas Gohr $data = auth_encrypt($data, auth_cookiesalt()); 465f044313dSAndreas Gohr return base64_encode($data); 466f044313dSAndreas Gohr } 467f044313dSAndreas Gohr 468f044313dSAndreas Gohr /** 469f044313dSAndreas Gohr * Decrypt the given string with the cookie salt 470f044313dSAndreas Gohr * 471f044313dSAndreas Gohr * @param string $data 472f044313dSAndreas Gohr * @return string 473f044313dSAndreas Gohr */ 47418622736SAndreas Gohr public function decrypt($data) 47518622736SAndreas Gohr { 476f044313dSAndreas Gohr $data = base64_decode($data); 47709870f99SPatrick Brown if ($data === false || $data === '') return false; 478f044313dSAndreas Gohr 479*09b1e97eSAndreas Gohr return auth_decrypt($data, auth_cookiesalt()); 480f044313dSAndreas Gohr } 481*09b1e97eSAndreas Gohr 482*09b1e97eSAndreas Gohr /** 483*09b1e97eSAndreas Gohr * Adds random space characters within the given text 484*09b1e97eSAndreas Gohr * 485*09b1e97eSAndreas Gohr * Keeps subsequent numbers without spaces (for math problem) 486*09b1e97eSAndreas Gohr * 487*09b1e97eSAndreas Gohr * @param $text 488*09b1e97eSAndreas Gohr * @return string 489*09b1e97eSAndreas Gohr */ 490*09b1e97eSAndreas Gohr protected function obfuscateText($text) 491*09b1e97eSAndreas Gohr { 492*09b1e97eSAndreas Gohr $new = ''; 493*09b1e97eSAndreas Gohr 494*09b1e97eSAndreas Gohr $spaces = [ 495*09b1e97eSAndreas Gohr "\r", 496*09b1e97eSAndreas Gohr "\n", 497*09b1e97eSAndreas Gohr "\r\n", 498*09b1e97eSAndreas Gohr ' ', 499*09b1e97eSAndreas Gohr "\xC2\xA0", 500*09b1e97eSAndreas Gohr // \u00A0 NO-BREAK SPACE 501*09b1e97eSAndreas Gohr "\xE2\x80\x80", 502*09b1e97eSAndreas Gohr // \u2000 EN QUAD 503*09b1e97eSAndreas Gohr "\xE2\x80\x81", 504*09b1e97eSAndreas Gohr // \u2001 EM QUAD 505*09b1e97eSAndreas Gohr "\xE2\x80\x82", 506*09b1e97eSAndreas Gohr // \u2002 EN SPACE 507*09b1e97eSAndreas Gohr // "\xE2\x80\x83", // \u2003 EM SPACE 508*09b1e97eSAndreas Gohr "\xE2\x80\x84", 509*09b1e97eSAndreas Gohr // \u2004 THREE-PER-EM SPACE 510*09b1e97eSAndreas Gohr "\xE2\x80\x85", 511*09b1e97eSAndreas Gohr // \u2005 FOUR-PER-EM SPACE 512*09b1e97eSAndreas Gohr "\xE2\x80\x86", 513*09b1e97eSAndreas Gohr // \u2006 SIX-PER-EM SPACE 514*09b1e97eSAndreas Gohr "\xE2\x80\x87", 515*09b1e97eSAndreas Gohr // \u2007 FIGURE SPACE 516*09b1e97eSAndreas Gohr "\xE2\x80\x88", 517*09b1e97eSAndreas Gohr // \u2008 PUNCTUATION SPACE 518*09b1e97eSAndreas Gohr "\xE2\x80\x89", 519*09b1e97eSAndreas Gohr // \u2009 THIN SPACE 520*09b1e97eSAndreas Gohr "\xE2\x80\x8A", 521*09b1e97eSAndreas Gohr // \u200A HAIR SPACE 522*09b1e97eSAndreas Gohr "\xE2\x80\xAF", 523*09b1e97eSAndreas Gohr // \u202F NARROW NO-BREAK SPACE 524*09b1e97eSAndreas Gohr "\xE2\x81\x9F", 525*09b1e97eSAndreas Gohr // \u205F MEDIUM MATHEMATICAL SPACE 526*09b1e97eSAndreas Gohr "\xE1\xA0\x8E\r\n", 527*09b1e97eSAndreas Gohr // \u180E MONGOLIAN VOWEL SEPARATOR 528*09b1e97eSAndreas Gohr "\xE2\x80\x8B\r\n", 529*09b1e97eSAndreas Gohr // \u200B ZERO WIDTH SPACE 530*09b1e97eSAndreas Gohr "\xEF\xBB\xBF\r\n", 531*09b1e97eSAndreas Gohr ]; 532*09b1e97eSAndreas Gohr 533*09b1e97eSAndreas Gohr $len = strlen($text); 534*09b1e97eSAndreas Gohr for ($i = 0; $i < $len - 1; $i++) { 535*09b1e97eSAndreas Gohr $new .= $text[$i]; 536*09b1e97eSAndreas Gohr 537*09b1e97eSAndreas Gohr if (!is_numeric($text[$i + 1])) { 538*09b1e97eSAndreas Gohr $new .= $spaces[array_rand($spaces)]; 539*09b1e97eSAndreas Gohr } 540*09b1e97eSAndreas Gohr } 541*09b1e97eSAndreas Gohr $new .= $text[$len - 1]; 542*09b1e97eSAndreas Gohr return $new; 543f044313dSAndreas Gohr } 544969b14c4SAndreas Gohr 54563609b6eSAndreas Gohr 54663609b6eSAndreas Gohr /** 54763609b6eSAndreas Gohr * Join multiple wav files 54863609b6eSAndreas Gohr * 54963609b6eSAndreas Gohr * All wave files need to have the same format and need to be uncompressed. 55063609b6eSAndreas Gohr * The headers of the last file will be used (with recalculated datasize 55163609b6eSAndreas Gohr * of course) 55263609b6eSAndreas Gohr * 55363609b6eSAndreas Gohr * @link http://ccrma.stanford.edu/CCRMA/Courses/422/projects/WaveFormat/ 55463609b6eSAndreas Gohr * @link http://www.thescripts.com/forum/thread3770.html 55563609b6eSAndreas Gohr */ 55663609b6eSAndreas Gohr protected function joinwavs($wavs) 55763609b6eSAndreas Gohr { 558*09b1e97eSAndreas Gohr $fields = implode( 559*09b1e97eSAndreas Gohr '/', 560*09b1e97eSAndreas Gohr [ 56163609b6eSAndreas Gohr 'H8ChunkID', 56263609b6eSAndreas Gohr 'VChunkSize', 56363609b6eSAndreas Gohr 'H8Format', 56463609b6eSAndreas Gohr 'H8Subchunk1ID', 56563609b6eSAndreas Gohr 'VSubchunk1Size', 56663609b6eSAndreas Gohr 'vAudioFormat', 56763609b6eSAndreas Gohr 'vNumChannels', 56863609b6eSAndreas Gohr 'VSampleRate', 56963609b6eSAndreas Gohr 'VByteRate', 57063609b6eSAndreas Gohr 'vBlockAlign', 571*09b1e97eSAndreas Gohr 'vBitsPerSample' 572*09b1e97eSAndreas Gohr ] 57363609b6eSAndreas Gohr ); 57463609b6eSAndreas Gohr 57563609b6eSAndreas Gohr $data = ''; 57663609b6eSAndreas Gohr foreach ($wavs as $wav) { 57763609b6eSAndreas Gohr $fp = fopen($wav, 'rb'); 57863609b6eSAndreas Gohr $header = fread($fp, 36); 57963609b6eSAndreas Gohr $info = unpack($fields, $header); 58063609b6eSAndreas Gohr 58163609b6eSAndreas Gohr // read optional extra stuff 58263609b6eSAndreas Gohr if ($info['Subchunk1Size'] > 16) { 58363609b6eSAndreas Gohr $header .= fread($fp, ($info['Subchunk1Size'] - 16)); 58463609b6eSAndreas Gohr } 58563609b6eSAndreas Gohr 58663609b6eSAndreas Gohr // read SubChunk2ID 58763609b6eSAndreas Gohr $header .= fread($fp, 4); 58863609b6eSAndreas Gohr 58963609b6eSAndreas Gohr // read Subchunk2Size 59063609b6eSAndreas Gohr $size = unpack('vsize', fread($fp, 4)); 59163609b6eSAndreas Gohr $size = $size['size']; 59263609b6eSAndreas Gohr 59363609b6eSAndreas Gohr // read data 59463609b6eSAndreas Gohr $data .= fread($fp, $size); 59563609b6eSAndreas Gohr } 59663609b6eSAndreas Gohr 59763609b6eSAndreas Gohr return $header . pack('V', strlen($data)) . $data; 59863609b6eSAndreas Gohr } 59963609b6eSAndreas Gohr 600*09b1e97eSAndreas Gohr // endregion 60177e00bf9SAndreas Gohr} 602