[grisbi-cvs] grisbi-web/newsportal/lib/captcha +cookie.patch, NONE, 1.1 COLLEGE.ttf, NONE, 1.1 README, NONE, 1.1 _test.1.php, NONE, 1.1 captcha.php, NONE, 1.1

NIEL GĂ©rald gegeweb at users.sourceforge.net
Mon May 24 17:03:52 CEST 2010


Update of /cvsroot/grisbi/grisbi-web/newsportal/lib/captcha
In directory sfp-cvsdas-2.v30.ch3.sourceforge.com:/tmp/cvs-serv18681/lib/captcha

Added Files:
	+cookie.patch COLLEGE.ttf README _test.1.php captcha.php 
Log Message:

news version off newsportal

--- NEW FILE: README ---

minimal CAPTCHA class
---------------------

As you might know, this PHP script provides a graphical human
verification test, which can be integrated into web submission
forms. A CAPTCHA (Completely Automated Public Turing to tell
Computers from Humans Apart) helps enormous to differentiate
real people from unwanted bots/spiders.

Link spammers preferrably target unlocked/open web sites ("the
editable and user-driven Web"), like comment features in blogs,
forum software, boards and Wikis of course. Often simple check-
boxes suffice to get rid of comment-spambots, but for really
motivated attackers only such a CAPTCHA can prevent massive page
corruption.

It should however be used as last resort only, because it hinders
visually impaired from using your site. This variant throws out
some readable text behind the generated image, but that won't
help much.


Integration
-----------

The static class contained within the script is pretty easy to
use.
On a page that contains a form (maybe even just a login or
registration mask), you simply call the ::form() function to get
a CAPTCHA image and input box generated:

 <form action="...
   <textarea>...
   ...
   <?php
     echo captcha::form();
   ?>
   ...
   <input type="submit"...
 </form>

The generated graphic will be linked or directly embedded into the
page (if you enabled the "data:"-URIs) and you do not need to care
about the additional form fields either.

On the receiving CGI/PHP script, you then simply invoke the
"captcha::check()" function, which will return a boolean value of
true, if the user correctly specified the displayed letters and
numbers.
You then simply proceed with processing the submitted <form>
data, and for example store the given text into the database or
so.

The ::check() function knows the correct POST variables, you don't
have to care here either.

Your application and form processing logic must however accomodate
to not call the CAPTCHA form generation or check repeatedly, but
that it also cannot be circumvented with another hidden form field
(which holds a multi-page form processing state e.g.).

It is recommended to simply integrate this into existing forms, but
you could also use it as a secondary pre-login form (= first the
captcha, then the real form). But take care, that it's often safer
and simpler to integrate the image+check() with another form.


Customization
-------------

Take care to prepare at least one .ttf (font) file. There is one
distributed with the tarball, which you should use (it's a very
small freeware font, which looks good for our purposes). It
should be placed in the same directory as the "captcha.php" script
so it can be found easily.
Else define() the "EWIKI_FONT_DIR" constant to something else (the
EWIKI here just refers to where it was first used, don't care).

Take note that the "captcha.php" script has some built-in defaults
which filter out certain possible letters from the CAPTCHAs. This
is because those don't look well with the default "COLLEGE.ttf"
font. You may want to tweak the ::mkpass() function, if you choose
a different font.

To make the CAPTCHA better match your site look and feel, you could
set the CAPTCHA_INVERSE constant to 1. This will yield a dark
image, whereas the default 0 would make it generate a bright/white
graphic.

Moreover you can style the form field from within your stylesheet
using simply the class ".captcha". There is a table within it, and
of course the input box - though that has a few direct formatting
styles set.

When invoking captcha::form() you could also override the default
text strings. The first sets the "-> retype that here" title, and
a second the explanaition on the right (you could for example use
"" as ::form()s second param to remove it).

Because there is probably no need to upgrade this ever(!) again,
you should do all customizations within the code. The size of the
generated images can only be changed that way for example.


Safety
------

The colors choosen in the CAPTCHA make it easier to read, but of
course also easy to scan in fact. Though dissecting such images
proves difficult, it is actually possible to do. And for this
captcha class it's easier, because of the easily distinguishable
colorspaces used. You probably notice that with the _INVERSE
version.

I don't want to scare you; this class will work - it's yet too
difficult for link spammers to program bots which could overcome
it; but be aware, that it is indeed possible.

What's more, the generated alt= text could be deciphered. As you
will see in the source it's actually a rather weird text, and so
decoding would be unpleasent; but it's possible nevertheless.

We use a MD5 hashsum for the actual text data, so that's surely
the safest part of this script ;)  The code, btw, will time out
after a few hours - replay attacks are possible, but only within a
limited time frame of ca. 3 hours. We skimp on a tracking database
table by that.



Advanced integration
--------------------

This captcha is likely to frustrate users, if you keep it enabled
everywhere and always - you should take precautions to disable the
captcha check automatically.

Set a cookie once it was solved, so your users only need to see
it once in a month (hint: good cookies shouldn't have a lifetime
longer than that, and sessions are evil, btw).


Compatibility
-------------

Since version 0.9 the generated CAPTCHA images get stored into
temporary files - like everyone else does.

You can however still enable (CAPTCHA_DATA_URLS) the embedding of
image data directly into pages. This is not compatible to all browsers
and MSIE in particular, even though this was standardized back in 1998
(see RFC2397).
The JavaScript/ActiveX workaround for MSIE has been removed from this
version, because the temporary file method is more reliable anyhow.

Other browsers (even text-based like 'w3m') do not have problems with
data:-URLs, so if you do not need to support MSIE users, it is still
recommended to enable it, because it enhances security slightly (does
not allow to fill a servers hard disk with temporary files from extra
page requests).


License
-------

This mini-script is Public Domain -> "free" as in dictionary.

If you rebrand, repackage and redistribute it, you're however politely
asked to modify the fragments in the ::textual_riddle() function. Too
widespread adoption would allow bot writers to overcome it else.


Notes
-----

Some boringly technical implementation notes and lame excuses for how
things were done here:

- downscaling the generated images to fit the desired $maxsize
   - needed for MSIE, which whines about lengthy <img src= URLs
   - works by accident - libgd's JPEG generation quality parameter scales
     down the file size nearly linearly
   - the * and - parameters are arbitrary, but now it mostly seems to
     need three trials only (that is really good)
   - matches the wanted length quite well (max 200 bytes away)
   - but regeneration of course adds to the overall wasted time


Alternatives
------------

There is a clean CAPTCHA test implementation in the PEAR collection,
http://pear.php.net/ - which is already used in many web apps.

On the <blink>free registration</blink>-phpclasses.org-site another
implementation (GPLed lib) can be read about. NucleusCMS ships with
it freely.

_This_ captcha.php class is used with ewiki+ for example (or there's
at least a plugin for it).

Other implementations are linked and documented on
http://en.wikipedia.org/wiki/Captcha


--- NEW FILE: _test.1.php ---
<html>
<head>
  <title>captcha.php testing</title>
  <style>
html {
   border-left: 30px solid #222222;
}
input,textarea {
   background: #333333;
   color: #dddddd;
   border: 2px solid #555555;
}
.captcha {
   border: 2px dashed: #331111;
   background: #110000;
   padding: 5px;
}
form {
   border: 1px dashed #551111;
   padding: 2px;
}
  </style>
</head>
<body bgcolor="#000000" text="#ffffff">
<h1>CAPTCHA Test</h1>
<?php
  define("CAPTCHA_INVERSE", 1);
  include "captcha.php";
?>

<form action="_test.1.php" >
(we have an example input &lt;form&gt; here)<br>
<textarea cols=50 rows=3><?php
  $res = captcha::check();
  if (isset($res)) {
     if ($res) {
        $msg = "SUCCESS!!";
     }
     else {
        $msg = "FAILED.";
     }
     for ($n=0; $n<5; $n++) {
        echo "$msg\n";
     }
  }
?></textarea>
<br>
<br>
<?php
  echo captcha::form();
?>
<br>
<input type="submit" value="Test &amp; Save">
</form>

<br>
<br>

This is just an example. It's function-less form, which appears over
and over again. You'd typically also want to provide a more senseful
error message, if captcha::test() fails.

<br>
<br>

</body>
</html>

--- NEW FILE: COLLEGE.ttf ---
(This appears to be a binary file; contents omitted.)

--- NEW FILE: +cookie.patch ---
# Apply this patch to the 'captcha.php' script to get built-in
# that disables the check automatically for two weeks (it sets
# a validation cookie).
#
#     patch < +cookie.patch
#
#
--- captcha.php	2005-08-09 09:41:09.000000000 +0000
+++ captcha+cookies.php	2005-08-09 09:42:36.000000000 +0000
@@ -25,6 +25,7 @@
 define("CAPTCHA_INVERSE", 0);                 // white or black(=1)
 define("CAPTCHA_TIMEOUT", 5000);              // in seconds (=max 4 hours)
 define("CAPTCHA_MAXSIZE", 4500);              // preferred image size
+define("CAPTCHA_COOKIE", "captcha_solved");   // to unlock captcha protection
 define("CAPTCHA_DATA_URLS", 0);               // RFC2397-URLs exclude MSIE users
 define("CAPTCHA_TEMP_DIR", "/tmp/captcha");
 define("CAPTCHA_BASE_URL", "http://$_SERVER[SERVER_NAME]:$_SERVER[SERVER_PORT]/" . substr(realpath(__FILE__), strlen($_SERVER["DOCUMENT_ROOT"])));
@@ -38,9 +39,17 @@
       verify input, @returns boolean
    */
    function check() {
+      $to = (int)(time()/1000000);
+      if ($_COOKIE[CAPTCHA_COOKIE] == $to) {
+         return(true);
+      }
       if (($hash = $_REQUEST["captcha_hash"])
       and ($pw = trim($_REQUEST["captcha_input"]))) {
-         return((captcha::hash($pw)==$hash) || (captcha::hash($pw,-1)==$hash));
+         $r = (captcha::hash($pw)==$hash) || (captcha::hash($pw,-1)==$hash);
+         if ($r) {
+            setcookie(CAPTCHA_COOKIE, $to, time()+1000000);
+         }
+         return($r);
       }
    }
 
@@ -50,6 +59,11 @@
    */
    function form($title="&rarr; retype that here", $more="<small>Enter the correct letters and numbers from the image into the text box. This small test serves as access restriction against malicious bots. Simply reload the page if this graphic is too hard to read.</small>") {
 
+      #-- stop if user already verified
+      if ($_COOKIE[CAPTCHA_COOKIE] == (int)(time()/1000000)) {
+         return "";
+      }
+
       #-- prepare image text
       $pw = captcha::mkpass();
       $hash = captcha::hash($pw);

--- NEW FILE: captcha.php ---
<?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 1 font type.
   
   Creates temporary files, which however get purged automatically after
   four hours.

   Public Domain, available via http://freshmeat.net/p/captchaphp
*/


#-- 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)
define("CAPTCHA_MAXSIZE", 4500);              // preferred image size
define("CAPTCHA_COOKIE", "captcha_solved");   // to unlock captcha protection
define("CAPTCHA_DATA_URLS", 0);               // RFC2397-URLs exclude MSIE users
define("CAPTCHA_TEMP_DIR", "/tmp/captcha");
define("CAPTCHA_BASE_URL", "http://$_SERVER[SERVER_NAME]:$_SERVER[SERVER_PORT]/" . substr(realpath(__FILE__), strlen($_SERVER["DOCUMENT_ROOT"])));


/* static - (you could instantiate it, but...) */
class captcha {


   /* gets parameter from $_REQUEST[] array (POST vars) and so can
      verify input, @returns boolean
   */
   function check() {
      $to = (int)(time()/1000000);
      if ($_COOKIE[CAPTCHA_COOKIE] == $to) {
         return(true);
      }
      if (($hash = $_REQUEST["captcha_hash"])
      and ($pw = trim($_REQUEST["captcha_input"]))) {
         $r = (captcha::hash($pw)==$hash) || (captcha::hash($pw,-1)==$hash);
         if ($r) {
            setcookie(CAPTCHA_COOKIE, $to, time()+1000000);
         }
         return($r);
      }
   }

   
   /* yields <input> fields html string (no complete form), with captcha
      image already embedded as data:-URI
   */
   function form($title="&rarr; retype that here", $more="<small>Enter the correct letters and numbers from the image into the text box. This small test serves as access restriction against malicious bots. Simply reload the page if this graphic is too hard to read.</small>") {

      #-- stop if user already verified
      if ($_COOKIE[CAPTCHA_COOKIE] == (int)(time()/1000000)) {
         return "";
      }

      #-- prepare image text
      $pw = captcha::mkpass();
      $hash = captcha::hash($pw);
      $alt = htmlentities(captcha::textual_riddle($pw));

      #-- image
      $img = captcha::image($pw, 200, 60, CAPTCHA_INVERSE, CAPTCHA_MAXSIZE);
      if (CAPTCHA_DATA_URLS && !strpos("MSIE", $_SERVER["HTTP_USER_AGENT"])) {
         $img_fn = "data:image/jpeg;base64," . base64_encode($img);
      }
      else {
         $img_fn = CAPTCHA_BASE_URL . "?_tcf=" . captcha::store_image($img);
      }
      
      #-- emit html form
      $html = 
        '<table border="0" summary="captcha input"><tr>'
      . '<td><img name="captcha_image" id="captcha_image" src="' .$img_fn. '" height="60" width="200" alt="'.$alt. '" /></td>'
      . '<td>'.$title. '<br/><input name="captcha_hash" type="hidden" value="'.$hash. '" />'
      . '<input name="captcha_input" type="text" size="7" maxlength="16" style="height:46px; font-size:34px; font-weight:450;" />'
      . '</td><td>'.$more.'</td>'
      . '</tr></table>';
      $html = "<div class=\"captcha\">$html</div>";
      return($html);
   }


   /* generates alternative (non-graphic), human-understandable
      representation of the passphrase
   */
   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";
         }
         #-- 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";
            }
         }
         #-- 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;
               }
            }
            while (rand(0,1));
            $add .= " = $n";
         }
         $s .= "$add";
         $s .= $symbols1[rand(0,count($symbols1)-1)] . "\n";
      }
      return($s);
   }


   /* returns jpeg file stream with unscannable letters encoded
      in front of colorful disturbing background
   */
   function image($phrase, $width=200, $height=60, $inverse=0, $maxsize=0xFFFFF) {
   
      #-- 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);
      
      #-- configuration
      $fonts = array(
        // "COLLEGE.ttf",
      );
      $fonts += glob(EWIKI_FONT_DIR."/*.ttf");
      
      #-- encolour bg
      $wd = 20;
      $x = 0;
      while ($x < $width) {
         imagefilledrectangle($img, $x, 0, $x+=$wd, $height, captcha::random_color($img, 222^$R, 255^$R));
         $wd += max(10, rand(0, 20) - 10);
      }

      #-- make interesting background I, lines
      $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));
         }
         if ($x < $height) {  // horizontally ("y")
            imageline($img, 0, $x-$w2, $width-1, $x-$w1, captcha::random_color($img,$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)) {
            break;
         }
      }
      
      #-- more disturbing II, random 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);
         $half = (int) ($size / 2);
         $x = rand(-$half, $width+$half);
         $y = rand(+$half, $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);
      }

      #-- add the real text to it
      $len = strlen($phrase);
      $w1 = 10;
      $w2 = $width / ($len+1);
      for ($p=0; $p<$len; $p++) {
         $letter = $phrase[$p];
         $size = rand(18, $height/2.2);
         $half = (int) $size / 2;
         $rotation = rand(-33, 33);
         $y = rand($size+3, $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
      $quality = 67;
      $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);
   }


   /* helper code */
   function random_color($img, $a,$b) {
      return imagecolorallocate($img, rand($a,$b), rand($a,$b), rand($a,$b));
   }
   
   
   /* creates temporary file, returns basename */
   function store_image($data) {
      $dir = CAPTCHA_TEMP_DIR;
      $id = md5($data);
   
      #-- create temp dir
      if (!file_exists($dir)) {
         mkdir($dir) && chmod($dir, 0777);
      }
      
      #-- remove stale files
      if ($dh = opendir($dir)) {
         $t_kill = time() - CAPTCHA_TIMEOUT;
         while($fn = readdir($dh)) if ($fn[0] != ".") {
            if (filemtime("$dir/$fn") < $t_kill) {
               unlink("$dir/$fn");
            }
         }
      }
      
      #-- store file
      fwrite($f = fopen("$dir/$id", "wb"), $data) && fclose($f);
      return($id);
   }

   /* sends it */
   function get_image($id) {
      $dir = CAPTCHA_TEMP_DIR;
      $fn = "$dir/$id";
      
      #-- find it
      if (preg_match('/^\w+$/', $id) && file_exists($fn)) {
         header("Content-Type: image/jpeg");
         header("Content-Length: " . filesize($fn));
         header("Last-Modified: " . gmstrftime("%a, %d %b %G %T %Z", filemtime($fn)));
         if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])) {
            header("Status: 304 Unmodified");
         }
         else {
            readfile($fn);
         }
         exit;
      }
   }


   /* unreversable hash from passphrase, with time() slice encoded */
   function hash($text, $dtime=0) {
      $text = strtolower($text);
      $pfix = (int) (time() / CAPTCHA_TIMEOUT) + $dtime;
      return md5("captcha::$pfix:$text::".__FILE__.":$_SERVER[SERVER_NAME]:80");
   }


   /* makes string of random letters for embedding into image and for
      encoding as hash, later verification
   */
   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);  // (depends on YOUR font)
      $s = substr($s, 0, rand(5,7));
      return($s);
   }
}


#-- IE workaround
if (isset($_REQUEST["_tcf"])) {
   captcha::get_image($_REQUEST["_tcf"]);
}


?>


More information about the cvs mailing list