diff options
author | pb1dft <pb1dft@ampache> | 2008-02-25 00:02:40 +0000 |
---|---|---|
committer | pb1dft <pb1dft@ampache> | 2008-02-25 00:02:40 +0000 |
commit | 73fee69c33df9e36801bfb5228577551504c075c (patch) | |
tree | 4f7c1163433d718593b4e7dc8c10c1e5671ac109 | |
parent | 6bd576a9fe2524f5c662767206efdebf0b43c8ab (diff) | |
download | ampache-73fee69c33df9e36801bfb5228577551504c075c.tar.gz ampache-73fee69c33df9e36801bfb5228577551504c075c.tar.bz2 ampache-73fee69c33df9e36801bfb5228577551504c075c.zip |
New Captcha Method
-rw-r--r-- | modules/captcha/captcha.php | 1267 | ||||
-rw-r--r-- | register.php | 2 |
2 files changed, 1064 insertions, 205 deletions
diff --git a/modules/captcha/captcha.php b/modules/captcha/captcha.php index aab8bab7..40963c18 100644 --- a/modules/captcha/captcha.php +++ b/modules/captcha/captcha.php @@ -1,289 +1,1148 @@ <?php /* - Does emit a CAPTCHA graphic and form fields, which allows to tell real - people from bots. - Though a textual description is generated as well, this sort of access - restriction will knock out visually impaired users, and frustrate all - others anyhow. Therefore this should only be used as last resort for - defending against spambots. Because of the readable text and the used - colorspaces this is a weak implementation, not completely OCR-secure. - - captcha::form() will return a html string to be inserted into textarea/ - [save] <forms> and alike. User input is veryfied with captcha::check(). - You should leave the sample COLLEGE.ttf next to this script, else you - have to define the _FONT_DIR constant correctly. Use only one type. - - Includes a sluggish workaround for Internet Explorer; but this script - must reside in a www-accessible directory then. + api: php + title: Easy_CAPTCHA + description: highly configurable, user-friendly and accessible CAPTCHA + version: 2.0 + author: milki + url: http://freshmeat.net/p/captchaphp + config: + <const name="CAPTCHA_PERSISTENT" value="1" type="boolean" title="persistent cookie" description="sets a cookie after user successfully solved it, spares further captchas for a few days" /> + <const name="CAPTCHA_NEW_URLS" value="1" type="boolean" title="new URLs only Javascript" description="uses Javascript detection to engage CAPTCHA only if a new URL was entered into any input box" /> + <const name="CAPTCHA_AJAX" value="1" type="boolean" title="AJAX quickcheck" description="verfies the solution (visually) while user enters it" /> + <const name="CAPTCHA_IMAGE_SIZE" value="200x60" type="string" regex="\d+x\d+" title="image size" description="height x width of CAPTCHA image" /> + <const name="CAPTCHA_INVERSE" value="1" type="boolean" title="inverse color" description="make captcha white on black" /> + <const name="CAPTCHA_PIXEL" value="1" type="multi" multi="1=single pixel|2=greyscale 2x2|3=smooth color" title="smooth drawing" description="image pixel assembly method and speed" /> + <const name="CAPTCHA_ONCLICK_HIRES" value="1" type="boolean" title="onClick-HiRes" description="reloads a finer resolution version of the CAPTCHA if user clicks on it" /> + <const name="CAPTCHA_TIMEOUT" value="5000" type="string" regex="\d+" title="verification timeout" description="in seconds, maxiumum time to elaps from CAPTCHA display to verification" /> + type: intercept + category: antispam + priority: optional + + + This library operates CAPTCHA form submissions, to block spam bots and + alike. It is easy to hook into existing web sites and scripts. It also + tries to be "smart" and more user-friendly. + + While the operation logic and identifier processing are extremley safe, + this is a "weak" implementation. Specifically targetted and tweaked OCR + software could overcome the visual riddle. And if enabled, the textual + or mathematical riddles are rather simple to overcome if attacked. + Generic spambots are however blocked already with the default settings. + + PRINT captcha::form() + emits the img and input fields for inclusion into your submit <form> + + IF (captcha::solved()) + tests for a correctly entered solution on submit, returns true if ok + + Temporary files are created for tracking, verification and basic data + storage, but will get automatically removed once a CAPTCHA was solved + to prevent replay attacks. Additionally this library uses "AJAX" super + powers *lol* to enhance usability. And a short-lasting session cookie + is also added site-wide, so users may only have to solve the captcha + once (can be disabled, because that's also just security by obscurity). + Public Domain, available via http://freshmeat.net/p/captchaphp */ +#-- behaviour +define("CAPTCHA_PERSISTENT", 0); // cookie-pass after it's solved once (does not work if headers were already sent on innovocation of captcha::solved() check) +define("CAPTCHA_NEW_URLS", 1); // force captcha only when URLs submitted +define("CAPTCHA_AJAX", 1); // visual feedback while entering letters +define("CAPTCHA_LOG", 0); // create /tmp/captcha/log file +define("CAPTCHA_NOTEXT", 0); // disables the accessible text/math riddle -#-- config -define("EWIKI_FONT_DIR", dirname(__FILE__)); // which fonts to use -define("CAPTCHA_INVERSE", 0); // white or black(=1) -define("CAPTCHA_TIMEOUT", 5000); // in seconds (=max 4 hours) +#-- look +define("CAPTCHA_IMAGE_TYPE", 2); // 1=wave, 2=whirly +define("CAPTCHA_INVERSE", 1); // white or black(=1) +define("CAPTCHA_IMAGE_SIZE", "200x60"); // randomly adapted a little +define("CAPTCHA_INPUT_STYLE", "height:46px; font-size:34px; font-weight:450;"); +define("CAPTCHA_PIXEL", 1); // set to 2 for smoother 2x2 grayscale pixel transform +define("CAPTCHA_ONCLICK_HIRES", 1); // use better/slower drawing mode on reloading +#-- solving +define("CAPTCHA_FUZZY", 0.65); // easier solving: accept 1 or 2 misguessed letters +define("CAPTCHA_TRIES", 5); // maximum failures for solving the captcha +define("CAPTCHA_AJAX_TRIES", 25); // AJAX testing limit (prevents brute-force cracking via check API) +define("CAPTCHA_MAXPASSES", 2); // 2 passes prevent user annoyment with caching/reload failures +define("CAPTCHA_TIMEOUT", 5000); // (in seconds/2) = 3:00 hours to solve a displayed captcha +define("CAPTCHA_MIN_CHARS", 7); // how many letters to use +define("CAPTCHA_MAX_CHARS", 9); -/* static - (you could instantiate it, but...) */ -class captcha { +#-- operation +define("CAPTCHA_TEMP_DIR", (@$_SERVER['TEMP'] ? $_SERVER['TEMP'] : '/tmp') . "/captcha/"); +define("CAPTCHA_PARAM_ID", "__ec_i"); +define("CAPTCHA_PARAM_INPUT", "__ec_s"); +define("CAPTCHA_BGCOLOR", 0xFFFFFF); // initial background color (non-inverse, white) +define("CAPTCHA_SALT", ",e?c:7<"); +#define("CAPTCHA_DATA_URLS", 0); // RFC2397-URLs exclude MSIE users +define("CAPTCHA_FONT_DIR", dirname(__FILE__)); +//define("CAPTCHA_BASE_URL", "http://$_SERVER[SERVER_NAME]:$_SERVER[SERVER_PORT]/" . substr(realpath(__FILE__), strlen(realpath($_SERVER["DOCUMENT_ROOT"])))); +define("CAPTCHA_BASE_URL", Config::get('web_path') . "/modules/captcha/captcha.php"); +/* simple API */ +class captcha { - /* gets parameter from $_REQUEST[] array (POST vars) and so can - verify input, @returns boolean - */ + #-- tests submitted CAPTCHA solution against tracking data + function solved() { + $c = new easy_captcha(); + return $c->solved(); + } function check() { - if (($hash = $_REQUEST["captcha_hash"]) - and ($pw = trim($_REQUEST["captcha_input"]))) { - return((captcha::hash($pw)==$hash) || (captcha::hash($pw,-1)==$hash)); + return captcha::solved(); + } + + #-- returns string with "<img> and <input>" fields for display in your <form> + function form($text="") { + $c = new easy_captcha(); + return $c->form("$text"); + } +} + + + +#-- init (triggered if *this* script is called directly) +if (realpath(strtok("$_SERVER[DOCUMENT_ROOT]/$_SERVER[REQUEST_URI]","?#"))==realpath(__FILE__)) { + easy_captcha_utility::API(); +} + + + + + + + + + + +/* base logic and data storare */ +class easy_captcha { + + + #-- init data + function easy_captcha($id=NULL, $ignore_expiration=0) { + + #-- load + if (($this->id = $id) or ($this->id = preg_replace("/[^-,.\w]+/", "", @$_REQUEST[CAPTCHA_PARAM_ID]))) { + $this->load(); + } + + #-- create new + if (empty($this->id) || !$ignore_expiration && !$this->is_valid() && $this->log("new()", "EXPIRED", "regenerating store")) { + $this->generate(); } } + + #-- create solutions + function generate() { + + #-- init + srand(microtime() + time()/2 - 21017); + if ($this->id) { $this->prev[] = $this->id; } + $this->id = $this->new_id(); + + #-- meta informations + $this->created = time(); + $this->{'created$'} = gmdate("r", $this->created); + $this->expires = $this->created + CAPTCHA_TIMEOUT; + //$this->tries = 0; + $this->passed = 0; + + #-- captcha processing info + $this->sent = 0; + $this->tries = CAPTCHA_TRIES; // 5 + $this->ajax_tries = CAPTCHA_AJAX_TRIES; // 25 + $this->passed = 0; + $this->maxpasses = CAPTCHA_MAXPASSES; // 2 + $this->failures = 0; + $this->shortcut = array(); + $this->grant = 0; // unchecked access + + #-- mk IMAGE/GRAPHIC + $this->image = (CAPTCHA_IMAGE_TYPE <= 1) + ? new easy_captcha_graphic_image_waved() + : new easy_captcha_graphic_image_disturbed(); + //$this->image = new easy_captcha_graphic_cute_ponys(); + + #-- mk MATH/TEXT riddle + $this->text = (CAPTCHA_NOTEXT >= 1) + ? new easy_captcha_text_disable() + : new easy_captcha_text_math_formula(); + //$this->text = new easy_captcha_text_riddle(); + + #-- process granting cookie + if (CAPTCHA_PERSISTENT) + $this->shortcut[] = new easy_captcha_persistent_grant(); + + #-- spam-check: no URLs submitted + if (CAPTCHA_NEW_URLS) + $this->shortcut[] = new easy_captcha_spamfree_no_new_urls(); + + #-- store record + $this->save(); + } - /* yields <input> fields html string (no complete form), with captcha - image already embedded as data:-URI - */ - public static function form($title="→ retype that here", $more="<small><br>Enter the correct letters and numbers from the image into the text box. <br>This small test serves as access restriction against malicious bots. <br>Simply reload the page if this graphic is too hard to read.</small>") { - $pw = captcha::mkpass(); - $hash = captcha::hash($pw); - $maxsize = (strpos("MSIE", $_SERVER["HTTP_USER_AGENT"]) ? 1000 : 6000); - @header("Vary: User-Agent"); - $img = "data:image/jpeg;base64," - . base64_encode(captcha::image($pw, 200, 60, CAPTCHA_INVERSE, $maxsize)); - $alt = htmlentities(captcha::textual_riddle($pw)); - $test = substr($img,22); - $html = - '<table summary="captcha input"><tr>' - . '<td><img name="captcha_image" id="captcha_image" src="'.$img. '" height="60" width="200" alt="'.$alt. '" /></td>' - . '<td>'.$title. '<br/><input name="captcha_hash" type="hidden" value="'.$hash. '" />' - . '<font color="red">*</font><input name="captcha_input" type="text" size="7" maxlength="16" style="height:23px; font-size:16px; font-weight:450;" />' - . '</td><td>'.$more.'</td>' - . '</tr></table>'; - - #-- js/html fix if ("MSIE") - { -// $base = "http://$_SERVER[SERVER_NAME]:$_SERVER[SERVER_PORT]".$conf('web_path')."/modules/captcha/captcha.php"; - $base = $_SERVER['PHP_SELF']; - $html .= <<<END -<script language="Javascript"><!-- -if (/Microsoft/.test(navigator.appName)) { -// var msg= "You are using IE Please download firefox in order to register. http://www.mozilla.org/products/firefox"; -// alert(msg); - var img = document.captcha_image; - img.src = "$base?_ddu=$test"; - //alert (img.src); - } ---></script> -END; - } - $html = "<div class=\"captcha\">$html</div>"; - return($html); - } - - - /* generates alternative (non-graphic), human-understandable - representation of the passphrase - */ - public static function textual_riddle($phrase) { - $symbols0 = '"\'-/_:'; - $symbols1 = array("\n,", "\n;", ";", "\n&", "\n-", ",", ",", "\nand then", "\nfollowed by", "\nand", "\nand not a\n\"".chr(65+rand(0,26))."\",\nbut"); - $s = "Guess the letters and numbers\n(passphrase riddle)\n--\n"; - for ($p=0; $p<strlen($phrase); $p++) { - $c = $phrase[$p]; - $add = ""; - #-- asis - if (!rand(0,3)) { - $i = $symbols0[rand(0,strlen($symbols0)-1)]; - $add = "$i$c$i"; + + #-- examine if captcha data is fresh + function is_valid() { + return isset($this->id) && ($this->created) + && ($this->expires > time()) + && ($this->tries > 0) + && ($this->failures < 500) + && ($this->passed < $this->maxpasses) + || $this->delete() || $this->log("is_valid", "EXPIRED", "and deleted") && false; + } + + + #-- new captcha tracking/storage id + function new_id() { + return "ec." . time() . "." . md5( $_SERVER["SERVER_NAME"] . CAPTCHA_SALT . rand(0,1<<30) ); + } + + + #-- check backends for correctness of solution + function solved() { + $ok = false; + + #-- failure + if ((0 >= $this->tries--) || !$this->is_valid()) { + // log, this is either a frustrated user or a bot knocking + $this->log("::solved", "INVALID", "tries exhausted ($this->tries) or expired(?) captcha"); + } + + #-- test + elseif ($this->sent) { + $in = $_REQUEST[CAPTCHA_PARAM_INPUT]; // might be empty string + + #-- check individual modules + $ok = $this->grant; + foreach ($this->shortcut as $test) { + $ok = $ok || $test->solved($in); // cookie & nourls } - #-- letter - elseif ($c >= 'A') { - $type = ($c >= 'a' ? "small " : ""); - do { - $n = rand(-3,3); - $c2 = chr((ord($c) & 0x5F) + $n); - } - while (($c2 < 'A') || ($c2 > 'Z')); - if ($n < 0) { - $n = -$n; - $add .= "$type'$c2' +$n letters"; - } - else { - $add .= "$n chars before $type$c2"; + $ok = $ok // either letters or math formula submitted + || isset($this->image) && $this->image->solved($in) + || isset($this->text) && $this->text->solved($in); + + #-- update state + if ($ok) { + $this->passed++; + $this->log("::solved", "OKAY", "captcha passed ($in) for image({$this->image->solution}) and text({$this->text->solution})"); + + #-- set cookie on success + if (CAPTCHA_PERSISTENT) { + $this->shortcut[0/*FIXME*/]->grant(); + $this->log("::solved", "PERSISTENT", "cookie granted"); } } - #-- number else { - $add = "???"; - $n = (int) $c; - do { - do { $x = rand(1, 10); } while (!$x); - $op = rand(0,11); - if ($op <= 2) { - $add = "($add * $x)"; $n *= $x; - } - elseif ($op == 3) { - $x = 2 * rand(1,2); - $add = "($add / $x)"; $n /= $x; - } - elseif ($sel % 2) { - $add = "($add + $x)"; $n += $x; - } - else { - $add = "($add - $x)"; $n -= $x; - } + $this->failures++; + $this->log("::solved", "WRONG", "solution failure ($in) for image({$this->image->solution}) and text({$this->text->solution})"); + } + } + + #-- remove if done + if (!$this->is_valid() /*&& !$this->delete()*/) { + $this->generate(); // ensure object instance can be reused - for quirky form processing logic + } + #-- store state/result + else { + $this->save(); + } + + #-- return result + return($ok); + } + + + #-- combines ->image and ->text data into form fields + function form($add_text="") { + + #-- store object data + $this->sent++; + $this->save(); + + #-- prepare output vars + $p_id = CAPTCHA_PARAM_ID; + $p_input = CAPTCHA_PARAM_INPUT; + $base_url = CAPTCHA_BASE_URL . '?' . CAPTCHA_PARAM_ID . '='; + $id = htmlentities($this->id); + $img_url = $base_url . $id; + $alt_text = htmlentities($this->text->question); + $new_urls = CAPTCHA_NEW_URLS ? 0 : 1; + $onClick = CAPTCHA_ONCLICK_HIRES ? 'onClick="this.src += this.src.match(/hires/) ? \'.\' : \'hires=1&\';"' : 'onClick="this.src += \'.\';"'; + $onKeyDown = CAPTCHA_AJAX ? 'onKeyDown="captcha_check_solution()"' : ''; + $javascript = CAPTCHA_AJAX ? '<script src="'.$base_url.'base.js&captcha_new_urls='.$new_urls.'" type="text/javascript" language="JavaScript" id="captcha_ajax_1"></script>' : ''; + + #-- assemble + $HTML = + '<div id="captcha" class="captcha">' . + '<input type="hidden" id="'.$p_id.'" name="'.$p_id.'" value="'.$id.'" />' . + '<img src="'.$img_url .'&" width="'.$this->image->width.'" height="'.$this->image->height.'" alt="'.$alt_text.'" align="middle" '.$onClick.' title="click on image to redraw" />' . + ' ' . + $add_text . + '<input title="please enter the letters you recognize in the CAPTCHA image to the left" type="text" '.$onKeyDown.' id="'.$p_input.'" name="'.$p_input.'" value="'.htmlentities($_REQUEST[$p_input]) . + '" size="8" style="'.CAPTCHA_INPUT_STYLE.'" />' . + $javascript . + '</div>'; + + return($HTML); + } + + + + #-- noteworthy stuff goes here + function log($error, $category, $message) { + // append to text file + if (CAPTCHA_LOG && ($log = @fopen(CAPTCHA_TEMP_DIR . "log", "a+"))) { + flock($log, LOCK_EX); + fwrite($log, "[$error] -$category- \"$message\" $_SERVER[REMOTE_ADDR] id={$this->id} tries={$this->tries} failures={$this->failures} created/time/expires=$this->created/".time()."/$this->expires \n"); + fclose($log); + } + return(TRUE); // for if-chaining + } + + + #-- load object from saved captcha tracking data + function load() { + $fn = $this->data_file(); + if (file_exists($fn)) { + $saved = (array)@unserialize(fread(fopen($fn, "r"), 1<<20)); + foreach ($saved as $i=>$v) { + $this->{$i} = $v; + } + } + else { + // log + } + } + + #-- save $this captcha state + function save() { + $this->straighten_temp_dir(); + if ($fn = $this->data_file()) { + $f = fopen($fn, "a"); + if (flock($f, LOCK_EX)) { + ftruncate($f, 0); + fwrite($f, serialize($this)); + } + fclose($f); + } + } + + #-- remove $this data file + function delete() { + // delete current and all previous data files + $this->prev[] = $this->id; + if (isset($this->prev)) { + foreach ($this->prev as $id) { + @unlink($this->data_file($id)); + } + } + // clean object + foreach ((array)$this as $name=>$val) { + unset($this->{$name}); + } + return(FALSE); // far if-chaining in ->is_valid() + } + + #-- clean-up or init temporary directory + function straighten_temp_dir() { + // create dir + if (!file_exists($dir=CAPTCHA_TEMP_DIR)) { + mkdir($dir); + } + // clean up old files + if ((!rand(0,50)) && ($dh = opendir($dir))) { + $t_kill = time() - CAPTCHA_TIMEOUT * 2; + while($fn = readdir($dh)) if ($fn[0] != ".") { + if (filemtime("$dir/$fn") < $t_kill) { + @unlink("$dir/$fn"); } - while (rand(0,1)); - $add .= " = $n"; } - $s .= "$add"; - $s .= $symbols1[rand(0,count($symbols1)-1)] . "\n"; } + } + + #-- where's the storage? + function data_file($id=NULL) { + return CAPTCHA_TEMP_DIR . preg_replace("/[^-,.\w]/", "", ($id?$id:$this->id)) . ".a()"; + } + + + #-- unreversable hash from passphrase, with time() slice encoded + function hash($text, $dtime=0, $length=1) { + $text = strtolower($text); + $pfix = (int) (time() / $length*CAPTCHA_TIMEOUT) + $dtime; + return md5("captcha::$pfix:$text::".__FILE__.":$_SERVER[SERVER_NAME]:80"); + } + +} + + + + + + + + + +#-- checks the supplied solution, allows differences (incorrectly guessed letters) +class easy_captcha_fuzzy extends easy_captcha { + + #-- ratio of letters that may differ between solution and real password + var $fuzzy = CAPTCHA_FUZZY; + + #-- compare + function solved($in) { + if ($in) { + $pw = strtolower($this->solution); + $in = strtolower($in); + $diff = levenshtein($pw, $in); + $maxdiff = strlen($pw) * (1 - $this->fuzzy); + return ($pw == $in) or ($diff <= $maxdiff); // either matches, or allows around 2 divergent letters + } + } + +} + + + + + + + + + +#-- image captchas, base and utility code +class easy_captcha_graphic extends easy_captcha_fuzzy { + + #-- config + function easy_captcha_graphic($x=NULL, $y=NULL) { + if (!$y) { + $x = strtok(CAPTCHA_IMAGE_SIZE, "x,|/*;:"); + $y = strtok(",."); + $x = rand($x * 0.9, $x * 1.2); + $y = rand($y - 5, $y + 15); + } + $this->width = $x; + $this->height = $y; + $this->inverse = CAPTCHA_INVERSE; + $this->bg = CAPTCHA_BGCOLOR; + $this->maxsize = 0xFFFFF; + $this->quality = 66; + $this->solution = $this->mkpass(); + } + + + #-- return a single .ttf font filename + function font() { + $fonts = array(/*"FreeMono-Medium.ttf"*/); + $fonts += glob(CAPTCHA_FONT_DIR."/*.ttf"); + return $fonts[rand(0,count($fonts)-1)]; + } + + + #-- makes string of random letters (for embedding into image) + function mkpass() { + $s = ""; + for ($n=0; $n<10; $n++) { + $s .= chr(rand(0, 255)); + } + $s = base64_encode($s); // base64-set, but filter out unwanted chars + $s = preg_replace("/[+\/=IG0ODQR]/i", "", $s); // strips hard to discern letters, depends on used font type + $s = substr($s, 0, rand(CAPTCHA_MIN_CHARS, CAPTCHA_MAX_CHARS)); return($s); } - /* returns jpeg file stream with unscannable letters encoded + #-- return GD color + function random_color($a,$b) { + $R = $this->inverse ? 0xFF : 0x00; + return imagecolorallocate($this->img, rand($a,$b)^$R, rand($a,$b)^$R, rand($a,$b)^$R); + } + function rgb ($r,$g,$b) { + $R = $this->inverse ? 0xFF : 0x00; + return imagecolorallocate($this->img, $r^$R, $g^$R, $b^$R); + } + + + #-- generate JPEG output + function output() { + ob_start(); + ob_implicit_flush(0); + imagejpeg($this->img, "", $this->quality); + $jpeg = ob_get_contents(); + ob_end_clean(); + imagedestroy($this->img); + unset($this->img); + return($jpeg); + } +} + + + + + + + + + +#-- waived captcha image II +class easy_captcha_graphic_image_waved extends easy_captcha_graphic { + + + /* returns jpeg file stream with unscannable letters encoded in front of colorful disturbing background */ - public static function image($phrase, $width=200, $height=60, $inverse=0, $maxsize=0xFFFFF) { + function jpeg() { + #-- step by step + $this->create(); + $this->text(); + //$this->debug_grid(); + $this->fog(); + $this->distort(); + return $this->output(); + } + + + #-- initialize in-memory image with gd library + function create() { + $this->img = imagecreatetruecolor($this->width, $this->height); + // imagealphablending($this->img, TRUE); + imagefilledrectangle($this->img, 0,0, $this->width,$this->height, $this->inverse ? $this->bg ^ 0xFFFFFF : $this->bg); //$this->rgb(255,255,255) + if (function_exists("imageantialias")) { + imageantialias($this->img, true); + } + } + + + #-- add the real text to it + function text() { + $w = $this->width; + $h = $this->height; + $SIZE = rand(30,36); + $DEG = rand(-2,9); + $LEN = strlen($this->solution); + $left = $w - $LEN * 25; + $top = ($h - $SIZE - abs($DEG*2)); + imagettftext($this->img, $SIZE, $DEG, rand(5,$left-5), $h-rand(3, $top-3), $this->rgb(0,0,0), $this->font(), $this->solution); + } + + #-- to visualize the sinus waves + function debug_grid() { + for ($x=0; $x<250; $x+=10) { + imageline($this->img, $x, 0, $x, 70, 0x333333); + imageline($this->img, 0, $x, 250, $x, 0x333333); + } + } - #-- initialize in-memory image with gd library - srand(microtime()*21017); - $img = imagecreatetruecolor($width, $height); - $R = $inverse ? 0xFF : 0x00; - imagefilledrectangle($img, 0,0, $width,$height, captcha::random_color($img, 222^$R, 255^$R)); - $c1 = rand(150^$R, 185^$R); - $c2 = rand(195^$R, 230^$R); + #-- add lines + function fog() { + $num = rand(10,25); + $x = $this->width; + $y = $this->height; + $s = rand(0,270); + for ($n=0; $n<$num; $n++) { + imagesetthickness($this->img, rand(1,2)); + imagearc($this->img, + rand(0.1*$x, 0.9*$x), rand(0.1*$y, 0.9*$y), //x,y + rand(0.1*$x, 0.3*$x), rand(0.1*$y, 0.3*$y), //w,h + $s, rand($s+5, $s+90), // s,e + rand(0,1) ? 0xFFFFFF : 0x000000 // col + ); + } + imagesetthickness($this->img, 1); + } + + + #-- distortion: wave-transform + function distort() { + + #-- init + $single_pixel = (CAPTCHA_PIXEL<=1); // very fast + $greyscale2x2 = (CAPTCHA_PIXEL<=2); // quicker than exact smooth 2x2 copy + $width = $this->width; + $height = $this->height; + $i = & $this->img; + $dest = imagecreatetruecolor($width, $height); - #-- configuration - $fonts = array(); - $fonts += glob(EWIKI_FONT_DIR."/*.ttf"); + #-- URL param ?hires=1 influences used drawing scheme + if (isset($_GET["hires"])) { + $single_pixel = 0; + } + + #-- prepare distortion + $wave = new easy_captcha_dxy_wave($width, $height); + $spike = new easy_captcha_dxy_spike($width, $height); + + #-- generate each new x,y pixel individually from orig $img + for ($y = 0; $y < $height; $y++) { + for ($x = 0; $x < $width; $x++) { + + #-- pixel movement + list($dx, $dy) = $wave->dxy($x, $y); // x- and y- sinus wave + // list($qx, $qy) = $spike->dxy($x, $y); + + #-- get source pixel, paint dest + if ($single_pixel) { + // single source dot: one-to-one duplicate (unsmooth, hard edges) + imagesetpixel($dest, $x, $y, @imagecolorat($i, (int)$dx+$x, (int)$dy+$y)); + } + elseif ($greyscale2x2) { + // merge 2x2 simple/greyscale (3 times as slow) + $cXY = $this->get_2x2_greyscale($i, $x+$dx, $y+$dy); + imagesetpixel($dest, $x,$y, imagecolorallocate($dest, $cXY, $cXY, $cXY)); + } + else { + // exact and smooth transformation (5 times as slow) + list($cXY_R, $cXY_G, $cXY_B) = $this->get_2x2_smooth($i, $x+$dx, $y+$dy); + imagesetpixel($dest, $x,$y, imagecolorallocate($dest, (int)$cXY_R, (int)$cXY_G, (int)$cXY_B)); + } + + } + } + + #-- simply overwrite ->img + imagedestroy($i); + $this->img = $dest; + } + + #-- get 4 pixels from source image, merges BLUE value simply + function get_2x2_greyscale(&$i, $x, $y) { + // this is pretty simplistic method, actually adds more artefacts + // than it "smoothes" + // it just merges the brightness from 4 adjoining pixels into one + $cXY = (@imagecolorat($i, $x+$dx, $y+$dy) & 0xFF) + + (@imagecolorat($i, $x+$dx, $y+$dy+1) & 0xFF) + + (@imagecolorat($i, $x+$dx+1, $y+$dy) & 0xFF) + + (@imagecolorat($i, $x+$dx+1, $y+$dy+1) & 0xFF); + $cXY = (int) ($cXY / 4); + return $cXY; + } + + #-- smooth pixel reading (with x,y being reals, not integers) + function get_2x2_smooth(&$i, $x, $y) { + // get R,G,B values from 2x2 source area + $c00 = $this->get_RGB($i, $x, $y); // +------+------+ + $c01 = $this->get_RGB($i, $x, $y+1); // |dx,dy | x1,y0| + $c10 = $this->get_RGB($i, $x+1, $y); // | rx-> | | + $c11 = $this->get_RGB($i, $x+1, $y+1); // +----##+------+ + // weighting by $dx/$dy fraction part // | ##|<-ry | + $rx = $x - floor($x); $rx_ = 1 - $rx; // |x0,y1 | x1,y1| + $ry = $y - floor($y); $ry_ = 1 - $ry; // +------+------+ + // this is extremely slow, but necessary for correct color merging, + // the source pixel lies somewhere in the 2x2 quadrant, that's why + // RGB values are added proportionately (rx/ry/_) + // we use no for-loop because that would slow it even further + $cXY_R = (int) (($c00[0]) * $rx_ * $ry_) + + (int) (($c01[0]) * $rx_ * $ry) // division by 4 not necessary, + + (int) (($c10[0]) * $rx * $ry_) // because rx/ry/rx_/ry_ add up + + (int) (($c11[0]) * $rx * $ry); // to 255 (=1.0) at most + $cXY_G = (int) (($c00[1]) * $rx_ * $ry_) + + (int) (($c01[1]) * $rx_ * $ry) + + (int) (($c10[1]) * $rx * $ry_) + + (int) (($c11[1]) * $rx * $ry); + $cXY_B = (int) (($c00[2]) * $rx_ * $ry_) + + (int) (($c01[2]) * $rx_ * $ry) + + (int) (($c10[2]) * $rx * $ry_) + + (int) (($c11[2]) * $rx * $ry); + return array($cXY_R, $cXY_G, $cXY_B); + } + + #-- imagegetcolor from current ->$img split up into RGB array + function get_RGB(&$img, $x, $y) { + $rgb = @imagecolorat($img, $x, $y); + return array(($rgb >> 16) &0xFF, ($rgb >>8) &0xFF, ($rgb) &0xFF); + } +} + + + + + + +#-- xy-wave deviation (works best for around 200x60) +# cos(x,y)-idea taken from imagemagick +class easy_captcha_dxy_wave { + + #-- init params + function easy_captcha_dxy_wave($max_x, $max_y) { + $this->dist_x = $this->real_rand(2.5, 3.5); // max +-x/y delta distance + $this->dist_y = $this->real_rand(2.5, 3.5); + $this->slow_x = $this->real_rand(7.5, 20.0); // =wave-width in pixel/3 + $this->slow_y = $this->real_rand(7.5, 15.0); + } + + #-- calculate source pixel position with overlapping sinus x/y-displacement + function dxy($x, $y) { + #-- adapting params + $this->dist_x *= 1.000035; + $this->dist_y *= 1.000015; + #-- dest pixels (with x+y together in each of the sin() calcs you get more deformation, else just yields y-ripple effect) + $dx = $this->dist_x * cos(($x/$this->slow_x) - ($y/1.1/$this->slow_y)); + $dy = $this->dist_y * sin(($y/$this->slow_y) - ($x/0.9/$this->slow_x)); + #-- result + return array($dx, $dy); + } + + #-- array of values with random start/end values + function from_to_rand($max, $a, $b) { + $BEG = $this->real_rand($a, $b); + $DIFF = $this->real_rand($a, $b) - $BEG; + $r = array(); + for ($i = 0; $i <= $max; $i++) { + $r[$i] = $BEG + $DIFF * $i / $max; + } + return($r); + } + + #-- returns random value in given interval + function real_rand($a, $b) { + $r = rand(0, 1<<30); + return( $r / (1<<30) * ($b-$a) + $a ); // base + diff * (0..1) + } +} + + +#-- with spike +class easy_captcha_dxy_spike { + function dxy($x,$y) { + #-- centre spike + $y += 0.0; + return array($x,$y); + } +} + + + + + + + + +#-- colorful captcha image I +class easy_captcha_graphic_image_disturbed extends easy_captcha_graphic { + + + /* returns jpeg file stream with unscannable letters encoded + in front of colorful disturbing background + */ + function jpeg() { + #-- step by step + $this->create(); + $this->background_lines(); + $this->background_letters(); + $this->text(); + return $this->output(); + } + + + #-- initialize in-memory image with gd library + function create() { + $this->img = imagecreatetruecolor($this->width, $this->height); + imagefilledrectangle($this->img, 0,0, $this->width,$this->height, $this->random_color(222, 255)); #-- encolour bg $wd = 20; $x = 0; - while ($x < $width) { - imagefilledrectangle($img, $x, 0, $x+=$wd, $height, captcha::random_color($img, 222^$R, 255^$R)); + while ($x < $this->width) { + imagefilledrectangle($this->img, $x, 0, $x+=$wd, $this->height, $this->random_color(222, 255)); $wd += max(10, rand(0, 20) - 10); } + } + - #-- make interesting background I, lines + #-- make interesting background I, lines + function background_lines() { + $c1 = rand(150, 185); + $c2 = rand(195, 230); $wd = 4; $w1 = 0; $w2 = 0; - for ($x=0; $x<$width; $x+=(int)$wd) { - if ($x < $width) { // verical - imageline($img, $x+$w1, 0, $x+$w2, $height-1, captcha::random_color($img,$c1,$c2)); + for ($x=0; $x<$this->width; $x+=(int)$wd) { + if ($x < $this->width) { // verical + imageline($this->img, $x+$w1, 0, $x+$w2, $this->height-1, $this->random_color($c1++,$c2)); } - if ($x < $height) { // horizontally ("y") - imageline($img, 0, $x-$w2, $width-1, $x-$w1, captcha::random_color($img,$c1,$c2)); + if ($x < $this->height) { // horizontally ("y") + imageline($this->img, 0, $x-$w2, $this->width-1, $x-$w1, $this->random_color($c1,$c2--)); } $wd += rand(0,8) - 4; if ($wd < 1) { $wd = 2; } $w1 += rand(0,8) - 4; $w2 += rand(0,8) - 4; - if (($x > $height) && ($y > $height)) { + if (($x > $this->height) && ($y > $this->height)) { break; } } + } + - #-- more disturbing II, random letters + #-- more disturbing II, random letters + function background_letters() { $limit = rand(30,90); for ($n=0; $n<$limit; $n++) { $letter = ""; do { $letter .= chr(rand(31,125)); // random symbol } while (rand(0,1)); - $size = rand(5, $height/2); + $size = rand(5, $this->height/2); $half = (int) ($size / 2); - $x = rand(-$half, $width+$half); - $y = rand(+$half, $height); + $x = rand(-$half, $this->width+$half); + $y = rand(+$half, $this->height); $rotation = rand(60, 300); - $c1 = captcha::random_color($img, 130^$R, 240^$R); - $font = $fonts[rand(0, count($fonts)-1)]; - imagettftext($img, $size, $rotation, $x, $y, $c1, $font, $letter); + imagettftext($this->img, $size, $rotation, $x, $y, $this->random_color(130, 240), $this->font(), $letter); } + } + - #-- add the real text to it + #-- add the real text to it + function text() { + $phrase = $this->solution; $len = strlen($phrase); $w1 = 10; - $w2 = $width / ($len+1); + $w2 = $this->width / ($len+1); for ($p=0; $p<$len; $p++) { $letter = $phrase[$p]; - $size = rand(18, $height/2.2); + $size = rand(18, $this->height/2.2); $half = (int) $size / 2; $rotation = rand(-33, 33); - $y = rand($size+3, $height-3); + $y = rand($size+3, $this->height-3); $x = $w1 + $w2*$p; - $w1 += rand(-$width/90, $width/40); // @BUG: last char could be +30 pixel outside of image - $font = $fonts[rand(0, count($fonts)-1)]; - $r=rand(30,99); $g=rand(30,99); $b=rand(30,99); // two colors for shadow - $c1 = imagecolorallocate($img, $r*1^$R, $g*1^$R, $b*1^$R); - $c2 = imagecolorallocate($img, $r*2^$R, $g*2^$R, $b*2^$R); - imagettftext($img, $size, $rotation, $x+1, $y, $c2, $font, $letter); - imagettftext($img, $size, $rotation, $x, $y-1, $c1, $font, $letter); - } - - #-- let JFIF stream be generated -/* Drop down quality if browser is MSIE */ -if (preg_match('|MSIE ([0-9].[0-9]{1,2})|',$_SERVER["HTTP_USER_AGENT"],$matched)){ - $quality = 8; - } else { - $quality = 100; - } - - $s = array(); - do { - ob_start(); ob_implicit_flush(0); - imagejpeg($img, "", (int)$quality); - $jpeg = ob_get_contents(); ob_end_clean(); - $size = strlen($jpeg); - $s_debug[] = ((int)($quality*10)/10) . "%=$size"; - $quality = $quality * ($maxsize/$size) * 0.93 - 1.7; // -($quality/7.222)* - } - while (($size > $maxsize) && ($quality >= 16)); - imagedestroy($img); -#print_r($s_debug); - return($jpeg); + $w1 += rand(-$this->width/90, $this->width/40); // @BUG: last char could be +30 pixel outside of image + $font = $this->font(); + list($r,$g,$b) = array(rand(30,99), rand(30,99), rand(30,99)); + imagettftext($this->img, $size, $rotation, $x+1, $y, $this->rgb($r*2,$g*2,$b*2), $font, $letter); + imagettftext($this->img, $size, $rotation, $x, $y-1, $this->rgb($r,$g,$b), $font, $letter); + } } +} + + + + + - /* helper code */ - public static function random_color($img, $a,$b) { - return imagecolorallocate($img, rand($a,$b), rand($a,$b), rand($a,$b)); + + + +#-- arithmetic riddle +class easy_captcha_text_math_formula extends easy_captcha { + + var $question = "1+1"; + var $solution = "2"; + + #-- set up + function easy_captcha_text_math_formula() { + $this->question = "What is " . $this->create_formula() . " = "; + $this->solution = $this->calculate_formula($this->question); + // we could do easier with iterated formula+result generation here, of course + // but I had this code handy already ;) and it's easier to modify } + #-- simple IS-EQUAL check + function solved($result) { + return (int)$this->solution == (int)$result; + } - /* unreversable hash from passphrase, with time() slice encoded */ - public static function hash($text, $dtime=0) { - $text = strtolower($text); - $pfix = (int) (time() / CAPTCHA_TIMEOUT) + $dtime; - return md5("captcha::$pfix:$text::".__FILE__.":$_SERVER[SERVER_NAME]:80"); + #-- make new captcha formula string + function create_formula() { + $formula = array( + rand(20,100) . " / " . rand(2,10), + rand(50,150) . " - " . rand(2,100), + rand( 2,100) . " + " . rand(2,100), + rand( 2, 15) . " * " . rand(2,12), + rand( 5, 10) . " * " . rand(5,10) . " - " . rand(1,20), + rand(30,100) . " + " . rand(5,99) . " - " . rand(1,50), + // rand(20,100) . " / " . rand(2,10) . " + " . rand(1,50), + ); + return $formula[rand(0,count($formula)-1)]; } + #-- remove non-arithmetic characters + function clean($s) { + return preg_replace("/[^-+*\/\d]/", "", $s); + } - /* makes string of random letters for embedding into image and for - encoding as hash, later verification - */ - public static function mkpass() { - $s = ""; - for ($n=0; $n<10; $n++) { - $s .= chr(rand(0, 255)); + #-- "solve" simple calculations + function calculate_formula($formula) { + preg_match("#^(\d+)([-+/*])(\d+)([-+/*])?(\d+)?$#", $this->clean($formula), $uu); + @list($uu, $X, $op1, $Y, $op2, $Z) = $uu; + if ($Y) $calc = array( + "/" => $X / $Y, // PHP+ZendVM catches division by zero already, and CAPTCHA "attacker" would get no advantage herefrom anyhow + "*" => $X * $Y, + "+" => $X + $Y, + "-" => $X - $Y, + "*-" => $X * $Y - $Z, + "+-" => $X + $Y - $Z, + "/+" => $X / $Y + $Z, + ); + return( $calc[$op1.$op2] ? $calc[$op1.$op2] : rand(0,1<<23) ); + } + +} + +#-- to disable textual captcha part +class easy_captcha_text_disable extends easy_captcha { + var $question = ""; + function solved($in) { + return false; + } +} + + + + + + + + +#-- shortcut, allow access for an user if captcha was previously solved +# (should be identical in each instantiation, cookie is time-bombed) +class easy_captcha_persistent_grant extends easy_captcha { + + function easy_captcha_persistent_grant() { + } + + + #-- give ok, if captach had already been solved recently + function solved($ignore=0) { + if (CAPTCHA_PERSISTENT && isset($_COOKIE[$this->cookie()])) { + return in_array($_COOKIE[$this->cookie()], array($this->validity_token(), $this->validity_token(-1))); } - $s = base64_encode($s); // base64-set, but filter out unwanted chars - $s = preg_replace("/[+\/=IG0ODQR]/i", "", $s); // (depends on YOUR font) - $s = substr($s, 0, rand(5,7)); - return($s); + } + + #-- set captcha persistence cookie + function grant() { + if (!headers_sent()) { + setcookie($this->cookie(), $this->validity_token(), time() + 175*CAPTCHA_TIMEOUT); + } + else { + // $this->log("::grant", "COOKIES", "too late for cookies"); + } + } + + #-- pseudo password (time-bombed) + function validity_token($deviation=0) { + return easy_captcha::hash("PERSISTENCE", $deviation, $length=100); + } + function cookie() { + return "captcha_pass"; } } -#-- IE workaround -if (isset($_REQUEST["_ddu"])) { - header("Content-Type: image/jpeg"); - die(base64_decode(substr($_REQUEST["_ddu"], 0))); + + + + + + + + +#-- simply check if no URLs were submitted - that's what most spambots do, +# and simply grant access then +class easy_captcha_spamfree_no_new_urls { + + #-- you have to adapt this, to check for newly added URLs only, in Wikis e.g. + # - for simple comment submission forms, this default however suffices: + function solved($ignore=0) { + $r = !preg_match("#(https?://\w+[^/,.]+)#ims", serialize($_GET+$_POST), $uu); + return $r; + } } + + + + + + + + +#-- "AJAX" and utility code +class easy_captcha_utility { + + + + + #-- script was called directly + /*static*/ function API() { + + #-- load data + if ($id = @$_GET[CAPTCHA_PARAM_ID]) { + + #-- special case + if ($id == 'base.js') { + easy_captcha_utility::js_base(); + } + + else { + $c = new easy_captcha($id=NULL, $ignore_expiration=1); + $expired = !$c->is_valid(); + + #-- JS-RPC request, check entered solution on the fly + if ($test = @$_REQUEST[CAPTCHA_PARAM_INPUT]) { + + #-- check + if ($expired) { + } + if (0 >= $c->ajax_tries--) { + $c->log("::API", "JS-RPC", "ajax_tries exhausted ($c->ajax_tries)"); + } + $ok = $c->image->solved($test) || $c->text->solved($test); + + #-- sendresult + easy_captcha_utility::js_rpc($ok); + } + + #-- generate and send image file + else { + if ($expired) { + $type = "image/png"; + $bin = easy_captcha_utility::expired_png(); + } + else { + $type = "image/jpeg"; + $bin = $c->image->jpeg(); + } + header("Pragma: no-cache"); + header("Cache-Control: no-cache, no-store, must-revalidate, private"); + header("Expires: " . gmdate("r", time())); + header("Content-Length: " . strlen($bin)); + header("Content-Type: $type"); + print $bin; + } + } + exit; + } + } + + #-- hardwired error img + function expired_png() { + return base64_decode("iVBORw0KGgoAAAANSUhEUgAAADwAAAAUAgMAAACsbba6AAAADFBMVEUeEhFcMjGgWFf9jIrTTikpAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA3UlEQVQY01XPzwoBcRAH8F9RjpSTm9xR9qQwtnX/latX0DrsA3gC8QDK0QO4bv7UOtmM+x4oZ4X5FQc1hlb41dR8mm/9ZhT/P7X/dDcpZPU3FYft9kWbLuWp4Bgt9v1oGG07Ja8ojfjxQFym02DVmoixkV/m2JI/TUtefR7nD9rkrhkC+6D77/8mUhDvw0ymLPwxf8esghEFRq8hqKcu2iG16Vlun1zYTO7RwCeFyoJqAgC3LQwzYiCokDj0MWRxb+Z6R8mPJb8Q77zlPbuCoJE8a/t7P773uv36tdcTmsXfRycoRJ8AAAAASUVORK5CYII="); + } + + + + + + #-- send base javascript + function js_base() { + $captcha_new_urls = $_GET["captcha_new_urls"] ? 0 : 1; + $base_url = CAPTCHA_BASE_URL; + $param_id = "?" . CAPTCHA_PARAM_ID . "="; + $param_input = "&" . CAPTCHA_PARAM_INPUT . "="; + header("Content-Type: text/javascript"); + print<<<END_____BASE__BASE__BASE__BASE__BASE__BASE__BASE__BASE_____END + + +/* easy_captcha utility code */ + +// global vars +captcha_url_rx = /(https?:\/\/\w[^\/,\]\[=#]+)/ig; // +captcha_form_urls = new Array(); +captcha_sol_cb = ""; +captcha_rpc = 0; + +// set up watchers +if ($captcha_new_urls) { + window.setTimeout("captcha_form_urls = captcha_find_urls_in_form()", 500); + window.setInterval("captcha_spamfree_no_new_urls()", 3000); +} + +// scans for URLs in any of the form fields +function captcha_find_urls_in_form() { + var nf, ne, nv; + for (nf=0; nf<document.forms.length; nf++) { + for (ne=0; ne<document.forms[nf].elements.length; ne++) { + nv += "\\n" + document.forms[nf].elements[ne].value; + } + } + var r = nv.match(captcha_url_rx); + if (!r) { r = new Array(); } + return r; +} +// diff URL lists and hide captcha if nothing new was entered +function captcha_spamfree_no_new_urls() { + var has_new_urls = captcha_find_urls_in_form().join(",") != captcha_form_urls.join(","); + var s = document.getElementById("captcha").style; + if (s.opacity) { + s.opacity = has_new_urls ? "0.9" : "0.1"; + } + else { + s.display = has_new_urls ? "block" : "none"; + } +} + +// if a certain solution length is reached, check it remotely (JS-RPC) +function captcha_check_solution() { + var cid = document.getElementById("__ec_i"); + var inf = document.getElementById("__ec_s"); + var len = inf.value.length; + // visualize processissing + if (len >= 4) { + inf.style.border = "2px solid #FF9955"; + } + // if enough letters entered + if (len >= 5) { + // remove old <script> node + var scr; + if (src = document.getElementById("captcha_ajax_1")) { + src.parentNode.removeChild(src); + } + // create new <script> node, initiate JS-RPC call thereby + scr = document.createElement("script"); + scr.setAttribute("language", "JavaScript"); + scr.setAttribute("type", "text/javascript"); + scr.setAttribute("src", "$base_url" + "$param_id" + cid.value + "$param_input" + inf.value); + scr.setAttribute("id", "captcha_ajax_1"); + document.getElementById("captcha").appendChild(scr); + captcha_rpc = 1; + } + // visual feedback for editing + var col = 10 + len*3; + inf.style.background = "#"+col+col+col; +} + + +END_____BASE__BASE__BASE__BASE__BASE__BASE__BASE__BASE_____END; + } + + + + + + + + #-- response javascript + function js_rpc($yes) { + $yes = $yes ? 1 : 0; + header("Content-Type: text/javascript"); + print<<<END_____JSRPC__JSRPC__JSRPC__JSRPC__JSRPC__JSRPC_____END + + +// JS-RPC response +if (1) { + captcha_rpc = 0; + var inf = document.getElementById("__ec_s"); + inf.style.borderColor = $yes ? "#22AA22" : "#AA2222"; +} + + +END_____JSRPC__JSRPC__JSRPC__JSRPC__JSRPC__JSRPC_____END; + } + + + +} + + + + + + + ?> diff --git a/register.php b/register.php index 28981e30..2ea93fc3 100644 --- a/register.php +++ b/register.php @@ -68,7 +68,7 @@ switch ($_REQUEST['action']) { /* If we're using the captcha stuff */ if (Config::get('captcha_public_reg')) { - $captcha = captcha::check(); + $captcha = captcha::solved(); if(!isset ($captcha)) { Error::add('captcha',_('Error Captcha Required')); } |