177e00bf9SAndreas Gohr<?php 277e00bf9SAndreas Gohr/** 377e00bf9SAndreas Gohr * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 477e00bf9SAndreas Gohr * @author Andreas Gohr <andi@splitbrain.org> 577e00bf9SAndreas Gohr */ 67218f96cSAndreas Gohr 777e00bf9SAndreas Gohr// must be run within Dokuwiki 877e00bf9SAndreas Gohrif(!defined('DOKU_INC')) die(); 977e00bf9SAndreas Gohrif(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/'); 107218f96cSAndreas Gohr 1177e00bf9SAndreas Gohr 1277e00bf9SAndreas Gohrclass helper_plugin_captcha extends DokuWiki_Plugin { 1377e00bf9SAndreas Gohr 1423d379a8SAndreas Gohr protected $field_in = 'plugin__captcha'; 1523d379a8SAndreas Gohr protected $field_sec = 'plugin__captcha_secret'; 1623d379a8SAndreas Gohr protected $field_hp = 'plugin__captcha_honeypot'; 1723d379a8SAndreas Gohr 1823d379a8SAndreas Gohr /** 1923d379a8SAndreas Gohr * Constructor. Initializes field names 2023d379a8SAndreas Gohr */ 21104ec268SAndreas Gohr public function __construct() { 2223d379a8SAndreas Gohr $this->field_in = md5($this->_fixedIdent().$this->field_in); 2323d379a8SAndreas Gohr $this->field_sec = md5($this->_fixedIdent().$this->field_sec); 2423d379a8SAndreas Gohr $this->field_hp = md5($this->_fixedIdent().$this->field_hp); 2523d379a8SAndreas Gohr } 2623d379a8SAndreas Gohr 2777e00bf9SAndreas Gohr /** 2877e00bf9SAndreas Gohr * Check if the CAPTCHA should be used. Always check this before using the methods below. 2977e00bf9SAndreas Gohr * 3077e00bf9SAndreas Gohr * @return bool true when the CAPTCHA should be used 3177e00bf9SAndreas Gohr */ 32104ec268SAndreas Gohr public function isEnabled() { 3377e00bf9SAndreas Gohr if(!$this->getConf('forusers') && $_SERVER['REMOTE_USER']) return false; 3477e00bf9SAndreas Gohr return true; 3577e00bf9SAndreas Gohr } 3677e00bf9SAndreas Gohr 3777e00bf9SAndreas Gohr /** 3877e00bf9SAndreas Gohr * Returns the HTML to display the CAPTCHA with the chosen method 3977e00bf9SAndreas Gohr */ 40104ec268SAndreas Gohr public function getHTML() { 4177e00bf9SAndreas Gohr global $ID; 4277e00bf9SAndreas Gohr 4377e00bf9SAndreas Gohr $rand = (float) (rand(0, 10000)) / 10000; 449e312724SAndreas Gohr if($this->getConf('mode') == 'math') { 459e312724SAndreas Gohr $code = $this->_generateMATH($this->_fixedIdent(), $rand); 469e312724SAndreas Gohr $code = $code[0]; 479e312724SAndreas Gohr $text = $this->getLang('fillmath'); 48df8afac4SAndreas Gohr } elseif($this->getConf('mode') == 'question') { 49df8afac4SAndreas Gohr $text = $this->getConf('question'); 509e312724SAndreas Gohr } else { 5177e00bf9SAndreas Gohr $code = $this->_generateCAPTCHA($this->_fixedIdent(), $rand); 529e312724SAndreas Gohr $text = $this->getLang('fillcaptcha'); 539e312724SAndreas Gohr } 54f044313dSAndreas Gohr $secret = $this->encrypt($rand); 5577e00bf9SAndreas Gohr 5628c14643SAndreas Gohr $txtlen = $this->getConf('lettercount'); 5728c14643SAndreas Gohr 5877e00bf9SAndreas Gohr $out = ''; 5977e00bf9SAndreas Gohr $out .= '<div id="plugin__captcha_wrapper">'; 6023d379a8SAndreas Gohr $out .= '<input type="hidden" name="'.$this->field_sec.'" value="'.hsc($secret).'" />'; 619e312724SAndreas Gohr $out .= '<label for="plugin__captcha">'.$text.'</label> '; 6223d379a8SAndreas Gohr 6377e00bf9SAndreas Gohr switch($this->getConf('mode')) { 649e312724SAndreas Gohr case 'math': 659d6f09afSAndreas Gohr case 'text': 669d6f09afSAndreas Gohr $out .= $this->_obfuscateText($code); 6777e00bf9SAndreas Gohr break; 6877e00bf9SAndreas Gohr case 'js': 699d6f09afSAndreas Gohr $out .= '<span id="plugin__captcha_code">'.$this->_obfuscateText($code).'</span>'; 7077e00bf9SAndreas Gohr break; 7177e00bf9SAndreas Gohr case 'image': 7277e00bf9SAndreas Gohr $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&id='.$ID.'" '. 7377e00bf9SAndreas Gohr ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> '; 7477e00bf9SAndreas Gohr break; 7577e00bf9SAndreas Gohr case 'audio': 7677e00bf9SAndreas Gohr $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/img.php?secret='.rawurlencode($secret).'&id='.$ID.'" '. 7777e00bf9SAndreas Gohr ' width="'.$this->getConf('width').'" height="'.$this->getConf('height').'" alt="" /> '; 7877e00bf9SAndreas Gohr $out .= '<a href="'.DOKU_BASE.'lib/plugins/captcha/wav.php?secret='.rawurlencode($secret).'&id='.$ID.'"'. 7977e00bf9SAndreas Gohr ' class="JSnocheck" title="'.$this->getLang('soundlink').'">'; 8077e00bf9SAndreas Gohr $out .= '<img src="'.DOKU_BASE.'lib/plugins/captcha/sound.png" width="16" height="16"'. 8177e00bf9SAndreas Gohr ' alt="'.$this->getLang('soundlink').'" /></a>'; 8277e00bf9SAndreas Gohr break; 8352e95008SAndreas Gohr case 'figlet': 8452e95008SAndreas Gohr require_once(dirname(__FILE__).'/figlet.php'); 8552e95008SAndreas Gohr $figlet = new phpFiglet(); 8652e95008SAndreas Gohr if($figlet->loadfont(dirname(__FILE__).'/figlet.flf')) { 8752e95008SAndreas Gohr $out .= '<pre>'; 8852e95008SAndreas Gohr $out .= rtrim($figlet->fetch($code)); 8952e95008SAndreas Gohr $out .= '</pre>'; 9052e95008SAndreas Gohr } else { 9152e95008SAndreas Gohr msg('Failed to load figlet.flf font file. CAPTCHA broken', -1); 9252e95008SAndreas Gohr } 9352e95008SAndreas Gohr break; 9477e00bf9SAndreas Gohr } 95df8afac4SAndreas Gohr $out .= ' <input type="text" size="'.$txtlen.'" name="'.$this->field_in.'" class="edit" /> '; 9623d379a8SAndreas Gohr 9723d379a8SAndreas Gohr // add honeypot field 989d63c05fSlainme $out .= '<label class="no">'.$this->getLang('honeypot').'<input type="text" name="'.$this->field_hp.'" /></label>'; 9977e00bf9SAndreas Gohr $out .= '</div>'; 10077e00bf9SAndreas Gohr return $out; 10177e00bf9SAndreas Gohr } 10277e00bf9SAndreas Gohr 10377e00bf9SAndreas Gohr /** 10477e00bf9SAndreas Gohr * Checks if the the CAPTCHA was solved correctly 10577e00bf9SAndreas Gohr * 10677e00bf9SAndreas Gohr * @param bool $msg when true, an error will be signalled through the msg() method 10777e00bf9SAndreas Gohr * @return bool true when the answer was correct, otherwise false 10877e00bf9SAndreas Gohr */ 109104ec268SAndreas Gohr public function check($msg = true) { 110478e363cSAndreas Gohr global $INPUT; 111478e363cSAndreas Gohr 112478e363cSAndreas Gohr $code = ''; 113478e363cSAndreas Gohr $field_sec = $INPUT->str($this->field_sec); 114478e363cSAndreas Gohr $field_in = $INPUT->str($this->field_in); 115478e363cSAndreas Gohr $field_hp = $INPUT->str($this->field_hp); 116478e363cSAndreas Gohr 117478e363cSAndreas Gohr // reconstruct captcha from provided $field_sec 118478e363cSAndreas Gohr if($field_sec) { 119478e363cSAndreas Gohr $rand = $this->decrypt($field_sec); 1209e312724SAndreas Gohr 1219e312724SAndreas Gohr if($this->getConf('mode') == 'math') { 1229e312724SAndreas Gohr $code = $this->_generateMATH($this->_fixedIdent(), $rand); 1239e312724SAndreas Gohr $code = $code[1]; 124df8afac4SAndreas Gohr } elseif($this->getConf('mode') == 'question') { 125df8afac4SAndreas Gohr $code = $this->getConf('answer'); 1269e312724SAndreas Gohr } else { 12777e00bf9SAndreas Gohr $code = $this->_generateCAPTCHA($this->_fixedIdent(), $rand); 1289e312724SAndreas Gohr } 129478e363cSAndreas Gohr } 13077e00bf9SAndreas Gohr 131478e363cSAndreas Gohr // compare values 132478e363cSAndreas Gohr if(!$field_sec || 133478e363cSAndreas Gohr !$field_in || 134478e363cSAndreas Gohr utf8_strtolower($field_in) != utf8_strtolower($code) || 135478e363cSAndreas Gohr trim($field_hp) !== '' 13623d379a8SAndreas Gohr ) { 13777e00bf9SAndreas Gohr if($msg) msg($this->getLang('testfailed'), -1); 13877e00bf9SAndreas Gohr return false; 13977e00bf9SAndreas Gohr } 14077e00bf9SAndreas Gohr return true; 14177e00bf9SAndreas Gohr } 14277e00bf9SAndreas Gohr 14377e00bf9SAndreas Gohr /** 14477e00bf9SAndreas Gohr * Build a semi-secret fixed string identifying the current page and user 14577e00bf9SAndreas Gohr * 14677e00bf9SAndreas Gohr * This string is always the same for the current user when editing the same 14727d84d8dSAndreas Gohr * page revision, but only for one day. Editing a page before midnight and saving 14827d84d8dSAndreas Gohr * after midnight will result in a failed CAPTCHA once, but makes sure it can 14927d84d8dSAndreas Gohr * not be reused which is especially important for the registration form where the 15027d84d8dSAndreas Gohr * $ID usually won't change. 151104ec268SAndreas Gohr * 152104ec268SAndreas Gohr * @return string 15377e00bf9SAndreas Gohr */ 154104ec268SAndreas Gohr public function _fixedIdent() { 15577e00bf9SAndreas Gohr global $ID; 15677e00bf9SAndreas Gohr $lm = @filemtime(wikiFN($ID)); 15727d84d8dSAndreas Gohr $td = date('Y-m-d'); 15877e00bf9SAndreas Gohr return auth_browseruid(). 15977e00bf9SAndreas Gohr auth_cookiesalt(). 16027d84d8dSAndreas Gohr $ID.$lm.$td; 16177e00bf9SAndreas Gohr } 16277e00bf9SAndreas Gohr 16377e00bf9SAndreas Gohr /** 1649d6f09afSAndreas Gohr * Adds random space characters within the given text 1659d6f09afSAndreas Gohr * 1669d6f09afSAndreas Gohr * Keeps subsequent numbers without spaces (for math problem) 1679d6f09afSAndreas Gohr * 1689d6f09afSAndreas Gohr * @param $text 1699d6f09afSAndreas Gohr * @return string 1709d6f09afSAndreas Gohr */ 1719d6f09afSAndreas Gohr protected function _obfuscateText($text) { 1729d6f09afSAndreas Gohr $new = ''; 1739d6f09afSAndreas Gohr 1749d6f09afSAndreas Gohr $spaces = array( 1759d6f09afSAndreas Gohr "\r", 1769d6f09afSAndreas Gohr "\n", 1779d6f09afSAndreas Gohr "\r\n", 1789d6f09afSAndreas Gohr ' ', 1799d6f09afSAndreas Gohr "\xC2\xA0", // \u00A0 NO-BREAK SPACE 1809d6f09afSAndreas Gohr "\xE2\x80\x80", // \u2000 EN QUAD 1819d6f09afSAndreas Gohr "\xE2\x80\x81", // \u2001 EM QUAD 1829d6f09afSAndreas Gohr "\xE2\x80\x82", // \u2002 EN SPACE 1839d6f09afSAndreas Gohr // "\xE2\x80\x83", // \u2003 EM SPACE 1849d6f09afSAndreas Gohr "\xE2\x80\x84", // \u2004 THREE-PER-EM SPACE 1859d6f09afSAndreas Gohr "\xE2\x80\x85", // \u2005 FOUR-PER-EM SPACE 1869d6f09afSAndreas Gohr "\xE2\x80\x86", // \u2006 SIX-PER-EM SPACE 1879d6f09afSAndreas Gohr "\xE2\x80\x87", // \u2007 FIGURE SPACE 1889d6f09afSAndreas Gohr "\xE2\x80\x88", // \u2008 PUNCTUATION SPACE 1899d6f09afSAndreas Gohr "\xE2\x80\x89", // \u2009 THIN SPACE 1909d6f09afSAndreas Gohr "\xE2\x80\x8A", // \u200A HAIR SPACE 1919d6f09afSAndreas Gohr "\xE2\x80\xAF", // \u202F NARROW NO-BREAK SPACE 1929d6f09afSAndreas Gohr "\xE2\x81\x9F", // \u205F MEDIUM MATHEMATICAL SPACE 1939d6f09afSAndreas Gohr 1949d6f09afSAndreas Gohr "\xE1\xA0\x8E\r\n", // \u180E MONGOLIAN VOWEL SEPARATOR 1959d6f09afSAndreas Gohr "\xE2\x80\x8B\r\n", // \u200B ZERO WIDTH SPACE 1969d6f09afSAndreas Gohr "\xEF\xBB\xBF\r\n", // \uFEFF ZERO WIDTH NO-BREAK SPACE 1979d6f09afSAndreas Gohr ); 1989d6f09afSAndreas Gohr 1999d6f09afSAndreas Gohr $len = strlen($text); 2009d6f09afSAndreas Gohr for($i = 0; $i < $len - 1; $i++) { 2019d6f09afSAndreas Gohr $new .= $text{$i}; 2029d6f09afSAndreas Gohr 2039d6f09afSAndreas Gohr if(!is_numeric($text{$i + 1})) { 2049d6f09afSAndreas Gohr $new .= $spaces[array_rand($spaces)]; 2059d6f09afSAndreas Gohr } 2069d6f09afSAndreas Gohr } 2079d6f09afSAndreas Gohr $new .= $text{$len - 1}; 2089d6f09afSAndreas Gohr return $new; 2099d6f09afSAndreas Gohr } 2109d6f09afSAndreas Gohr 2119d6f09afSAndreas Gohr /** 212*a02b2219SPatrick Brown * Generate some numbers from a known string and random number 213*a02b2219SPatrick Brown * 214*a02b2219SPatrick Brown * @param $fixed string the fixed part, any string 215*a02b2219SPatrick Brown * @param $rand float some random number between 0 and 1 216*a02b2219SPatrick Brown * @return string 217*a02b2219SPatrick Brown */ 218*a02b2219SPatrick Brown private function _generateNumbers($fixed, $rand) { 219*a02b2219SPatrick Brown $fixed = hexdec(substr(md5($fixed), 5, 5)); // use part of the md5 to generate an int 220*a02b2219SPatrick Brown $rand = $rand * 0xFFFFF; // bitmask from the random number 221*a02b2219SPatrick Brown return md5($rand ^ $fixed); // combine both values 222*a02b2219SPatrick Brown } 223*a02b2219SPatrick Brown 224*a02b2219SPatrick Brown /** 22528c14643SAndreas Gohr * Generates a random char string 22677e00bf9SAndreas Gohr * 227104ec268SAndreas Gohr * @param $fixed string the fixed part, any string 228104ec268SAndreas Gohr * @param $rand float some random number between 0 and 1 22923d379a8SAndreas Gohr * @return string 23077e00bf9SAndreas Gohr */ 231104ec268SAndreas Gohr public function _generateCAPTCHA($fixed, $rand) { 232*a02b2219SPatrick Brown $numbers = $this->_generateNumbers($fixed, $rand); 23377e00bf9SAndreas Gohr 23477e00bf9SAndreas Gohr // now create the letters 23577e00bf9SAndreas Gohr $code = ''; 23628c14643SAndreas Gohr for($i = 0; $i < ($this->getConf('lettercount') * 2); $i += 2) { 23777e00bf9SAndreas Gohr $code .= chr(floor(hexdec($numbers[$i].$numbers[$i + 1]) / 10) + 65); 23877e00bf9SAndreas Gohr } 23977e00bf9SAndreas Gohr 24077e00bf9SAndreas Gohr return $code; 24177e00bf9SAndreas Gohr } 24277e00bf9SAndreas Gohr 2439e312724SAndreas Gohr /** 2449e312724SAndreas Gohr * Create a mathematical task and its result 2459e312724SAndreas Gohr * 246104ec268SAndreas Gohr * @param $fixed string the fixed part, any string 247104ec268SAndreas Gohr * @param $rand float some random number between 0 and 1 248104ec268SAndreas Gohr * @return array taks, result 2499e312724SAndreas Gohr */ 250104ec268SAndreas Gohr protected function _generateMATH($fixed, $rand) { 251*a02b2219SPatrick Brown $numbers = $this->_generateNumbers($fixed, $rand); 2529e312724SAndreas Gohr 2539e312724SAndreas Gohr // first letter is the operator (+/-) 2549e312724SAndreas Gohr $op = (hexdec($numbers[0]) > 8) ? -1 : 1; 2559e312724SAndreas Gohr $num = array(hexdec($numbers[1].$numbers[2]), hexdec($numbers[3])); 2569e312724SAndreas Gohr 2579e312724SAndreas Gohr // we only want positive results 2589e312724SAndreas Gohr if(($op < 0) && ($num[0] < $num[1])) rsort($num); 2599e312724SAndreas Gohr 2609e312724SAndreas Gohr // prepare result and task text 2619e312724SAndreas Gohr $res = $num[0] + ($num[1] * $op); 2629d6f09afSAndreas Gohr $task = $num[0].(($op < 0) ? '-' : '+').$num[1].'=?'; 2639e312724SAndreas Gohr 2649e312724SAndreas Gohr return array($task, $res); 2659e312724SAndreas Gohr } 26677e00bf9SAndreas Gohr 26777e00bf9SAndreas Gohr /** 26877e00bf9SAndreas Gohr * Create a CAPTCHA image 269104ec268SAndreas Gohr * 270104ec268SAndreas Gohr * @param string $text the letters to display 27177e00bf9SAndreas Gohr */ 272104ec268SAndreas Gohr public function _imageCAPTCHA($text) { 27377e00bf9SAndreas Gohr $w = $this->getConf('width'); 27477e00bf9SAndreas Gohr $h = $this->getConf('height'); 27577e00bf9SAndreas Gohr 276dc091fd0SAndreas Gohr $fonts = glob(dirname(__FILE__).'/fonts/*.ttf'); 277dc091fd0SAndreas Gohr 27877e00bf9SAndreas Gohr // create a white image 27928c14643SAndreas Gohr $img = imagecreatetruecolor($w, $h); 28028c14643SAndreas Gohr $white = imagecolorallocate($img, 255, 255, 255); 28128c14643SAndreas Gohr imagefill($img, 0, 0, $white); 28277e00bf9SAndreas Gohr 28377e00bf9SAndreas Gohr // add some lines as background noise 28477e00bf9SAndreas Gohr for($i = 0; $i < 30; $i++) { 28577e00bf9SAndreas Gohr $color = imagecolorallocate($img, rand(100, 250), rand(100, 250), rand(100, 250)); 28677e00bf9SAndreas Gohr imageline($img, rand(0, $w), rand(0, $h), rand(0, $w), rand(0, $h), $color); 28777e00bf9SAndreas Gohr } 28877e00bf9SAndreas Gohr 28977e00bf9SAndreas Gohr // draw the letters 29028c14643SAndreas Gohr $txtlen = strlen($text); 29128c14643SAndreas Gohr for($i = 0; $i < $txtlen; $i++) { 292dc091fd0SAndreas Gohr $font = $fonts[array_rand($fonts)]; 29377e00bf9SAndreas Gohr $color = imagecolorallocate($img, rand(0, 100), rand(0, 100), rand(0, 100)); 29477e00bf9SAndreas Gohr $size = rand(floor($h / 1.8), floor($h * 0.7)); 29577e00bf9SAndreas Gohr $angle = rand(-35, 35); 29677e00bf9SAndreas Gohr 29728c14643SAndreas Gohr $x = ($w * 0.05) + $i * floor($w * 0.9 / $txtlen); 29877e00bf9SAndreas Gohr $cheight = $size + ($size * 0.5); 29977e00bf9SAndreas Gohr $y = floor($h / 2 + $cheight / 3.8); 30077e00bf9SAndreas Gohr 30177e00bf9SAndreas Gohr imagettftext($img, $size, $angle, $x, $y, $color, $font, $text[$i]); 30277e00bf9SAndreas Gohr } 30377e00bf9SAndreas Gohr 30477e00bf9SAndreas Gohr header("Content-type: image/png"); 30577e00bf9SAndreas Gohr imagepng($img); 30677e00bf9SAndreas Gohr imagedestroy($img); 30777e00bf9SAndreas Gohr } 30877e00bf9SAndreas Gohr 309f044313dSAndreas Gohr /** 310f044313dSAndreas Gohr * Encrypt the given string with the cookie salt 311f044313dSAndreas Gohr * 312f044313dSAndreas Gohr * @param string $data 313f044313dSAndreas Gohr * @return string 314f044313dSAndreas Gohr */ 315f044313dSAndreas Gohr public function encrypt($data) { 316f044313dSAndreas Gohr if(function_exists('auth_encrypt')) { 317f044313dSAndreas Gohr $data = auth_encrypt($data, auth_cookiesalt()); // since binky 318f044313dSAndreas Gohr } else { 319f044313dSAndreas Gohr $data = PMA_blowfish_encrypt($data, auth_cookiesalt()); // deprecated 320f044313dSAndreas Gohr } 321f044313dSAndreas Gohr 322f044313dSAndreas Gohr return base64_encode($data); 323f044313dSAndreas Gohr } 324f044313dSAndreas Gohr 325f044313dSAndreas Gohr /** 326f044313dSAndreas Gohr * Decrypt the given string with the cookie salt 327f044313dSAndreas Gohr * 328f044313dSAndreas Gohr * @param string $data 329f044313dSAndreas Gohr * @return string 330f044313dSAndreas Gohr */ 331f044313dSAndreas Gohr public function decrypt($data) { 332f044313dSAndreas Gohr $data = base64_decode($data); 333f044313dSAndreas Gohr 334f044313dSAndreas Gohr if(function_exists('auth_decrypt')) { 335f044313dSAndreas Gohr return auth_decrypt($data, auth_cookiesalt()); // since binky 336f044313dSAndreas Gohr } else { 337f044313dSAndreas Gohr return PMA_blowfish_decrypt($data, auth_cookiesalt()); // deprecated 338f044313dSAndreas Gohr } 339f044313dSAndreas Gohr } 34077e00bf9SAndreas Gohr} 341