Added:
trunk/Tipos/
trunk/Tipos/Captcha/
trunk/Tipos/Captcha/Image.php
trunk/Tipos/Filter/
trunk/Tipos/Filter/ValidateCaptcha.php (contents, props changed)
trunk/Tipos/View/
trunk/Tipos/View/Helper/
trunk/Tipos/View/Helper/FormCaptcha.php
Log:
Added CAPTCHA creator and related form helper / filter to validate it.
Added: trunk/Tipos/Captcha/Image.php
==============================================================================
--- (empty file)
+++ trunk/Tipos/Captcha/Image.php Sun Oct 5 13:40:52 2008
@@ -0,0 +1,336 @@
+<?php
+/**
+ *
+ * Class to generate CAPTCHA's (Completely Automated Public Turing Test to
Tell
+ * Computers and Humans Apart). Based on KCAPTCHA class by Kruglov Sergei.
+ *
+ * http://www.captcha.ru/en/kcaptcha/
+ *
+ * @category Tipos
+ *
+ * @package Tipos_Captcha
+ *
+ * @author Rodrigo Moraes <rodrigo...@gmail.com>
+ *
+ * @copyright Kruglov Sergei, 2006, 2007
+ *
+ * @version $Id: Image.php 1464 2007-12-21 12:22:01Z rodrigo.moraes $
+ *
+ */
+class Tipos_Captcha_Image extends Solar_Base
+{
+ /**
+ *
+ *
+ *
+ *
+ *
+ */
+ protected $_Tipos_Captcha_Image = array(
+ 'save_path' => null,
+ 'font_path' => null,
+ 'hash_algo' => 'md5',
+ 'salt' => null,
+ 'length' => 5,
+ 'chars' => '23456789abcdeghkmnpqsuvxyz',
+ 'alphabet' => '0123456789abcdefghijklmnopqrstuvwxyz',
+ 'fonts' => array(
+ 'warnock.png', 'times.png', 'rockwell.png', 'perpetua.png',
+ 'palatino.png', 'minion.png', 'lucida.png', 'kozuka.png',
+ 'goudy_old.png', 'georgia.png', 'garamond.png', 'footlight.png',
+ 'constantia.png', 'chaparral.png', 'century.png', 'centaur.png',
+ 'cambria.png', 'calisto.png', 'bookman.png', 'batang.png',
+ 'baskerville.png', 'antiqua.png',
+ ),
+ 'width' => 120,
+ 'height' => 60,
+ 'fluctuation' => 2,
+ 'no_spaces' => false,
+ 'jpeg_quality' => 90,
+ );
+
+ /**
+ *
+ * A session object to set and check the hashed captcha solution.
+ *
+ * @var Solar_Session
+ *
+ */
+ protected $_session;
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param array $config User-provided configuration values.
+ *
+ */
+ public function __construct($config = null)
+ {
+ // do parent construction
+ parent::__construct($config);
+
+ $this->_session = Solar::factory('Solar_Session', array(
+ 'class' => 'Tipos_Captcha_Image'
+ ));
+ }
+
+ /**
+ *
+ * Creates a capcha image, adds the hashed solution to the session and
+ * returns the file name.
+ *
+ * @return string Captcha file name.
+ *
+ */
+ public function getCaptcha()
+ {
+ $length = $this->_config['length'];
+ $alphabet = $this->_config['alphabet'];
+ $font_path = $this->_config['font_path'];
+ $fonts = $this->_config['fonts'];
+ $width = $this->_config['width'];
+ $height = $this->_config['height'];
+ $fluctuation = $this->_config['fluctuation'];
+ $no_spaces = $this->_config['no_spaces'];
+ $jpeg_quality = $this->_config['jpeg_quality'];
+
+ $alphabet_length = strlen($alphabet);
+ $foreground_color = array(mt_rand(0,100), mt_rand(0,100),
mt_rand(0,100));
+ $background_color = array(mt_rand(200,255), mt_rand(200,255),
mt_rand(200,255));
+
+ $solution = $this->_getSolution();
+
+ while (true) {
+ $font_file = $font_path . $fonts[mt_rand(0, count($fonts) -
1)];
+ $font = imagecreatefrompng($font_file);
+ imagealphablending($font, true);
+ $fontfile_width = imagesx($font);
+ $fontfile_height = imagesy($font) - 1;
+ $font_metrics = array();
+ $symbol = 0;
+ $reading_symbol = false;
+
+ // loading font
+ for($i = 0; $i < $fontfile_width && $symbol <
$alphabet_length; $i++) {
+ $transparent = (imagecolorat($font, $i, 0) >> 24) == 127;
+
+ if (!$reading_symbol && !$transparent) {
+ $font_metrics[$alphabet{$symbol}] = array('start' =>
$i);
+ $reading_symbol = true;
+ continue;
+ }
+
+ if ($reading_symbol && $transparent) {
+ $font_metrics[$alphabet{$symbol}]['end'] = $i;
+ $reading_symbol = false;
+ $symbol++;
+ continue;
+ }
+ }
+
+ $img = imagecreatetruecolor($width, $height);
+ imagealphablending($img, true);
+ $white = imagecolorallocate($img, 255, 255, 255);
+ $black = imagecolorallocate($img, 0, 0, 0);
+
+ imagefilledrectangle($img, 0, 0, $width - 1, $height - 1,
$white);
+
+ // draw text
+ $x = 1;
+ for($i = 0; $i < $length; $i++) {
+ $m = $font_metrics[$solution{$i}];
+
+ $y = mt_rand(-$fluctuation, $fluctuation) + ($height -
$fontfile_height) / 2 + 2;
+
+ if ($no_spaces) {
+ $shift = 0;
+ if ($i>0) {
+ $shift = 1000;
+ for($sy = 7; $sy < $fontfile_height - 20; $sy +=
1) {
+ //for($sx = $m['start'] - 1; $sx < $m['end'];
$sx += 1) {
+ for($sx = $m['start'] - 1; $sx < $m['end'];
$sx += 1) {
+ $rgb = imagecolorat($font, $sx, $sy);
+ $opacity = $rgb >> 24;
+ if ($opacity < 127) {
+ $left = $sx - $m['start'] + $x;
+ $py = $sy + $y;
+ if ($py > $height) break;
+ for($px = min($left,$width - 1); $px >
$left - 12 && $px >= 0; $px -= 1) {
+ $color = imagecolorat($img, $px,
$py) & 0xff;
+ if ($color + $opacity < 190) {
+ if ($shift > $left - $px) {
+ $shift = $left - $px;
+ }
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+ if ($shift == 1000) {
+ $shift = mt_rand(4,6);
+ }
+
+ }
+ } else {
+ $shift = 1;
+ }
+ imagecopy($img, $font, $x - $shift, $y, $m['start'], 1,
$m['end'] - $m['start'], $fontfile_height);
+ $x += $m['end'] - $m['start'] - $shift;
+ }
+ if ($x < $width - 10) {
+ break; // fit in canvas
+ }
+
+ }
+ $center = $x / 2;
+
+ // fill background and foreground
+ $img2 = imagecreatetruecolor($width, $height);
+ $foreground = imagecolorallocate($img2, $foreground_color[0],
$foreground_color[1], $foreground_color[2]);
+ $background = imagecolorallocate($img2, $background_color[0],
$background_color[1], $background_color[2]);
+ imagefilledrectangle($img2, 0, 0, $width-1, $height-1,
$background);
+ imagefilledrectangle($img2, 0, $height, $width-1, $height-1,
$foreground);
+
+ // periods
+ $rand1 = mt_rand(750000, 1200000) / 10000000;
+ $rand2 = mt_rand(750000, 1200000) / 10000000;
+ $rand3 = mt_rand(750000, 1200000) / 10000000;
+ $rand4 = mt_rand(750000, 1200000) / 10000000;
+ // phases
+ $rand5 = mt_rand(0, 31415926) / 10000000;
+ $rand6 = mt_rand(0, 31415926) / 10000000;
+ $rand7 = mt_rand(0, 31415926) / 10000000;
+ $rand8 = mt_rand(0, 31415926) / 10000000;
+ // amplitudes
+ $rand9 = mt_rand(330, 420) / 110;
+ $rand10 = mt_rand(330, 450) / 110;
+
+ // wave distortion
+ for($x = 0; $x < $width; $x++) {
+ for($y = 0; $y < $height; $y++) {
+ $sx = $x + (sin($x * $rand1 + $rand5) + sin($y * $rand3 +
$rand6)) * $rand9 - $width / 2 + $center + 1;
+ $sy = $y + (sin($x * $rand2 + $rand7) + sin($y * $rand4 +
$rand8)) * $rand10;
+
+ if ($sx < 0 || $sy < 0 || $sx >= $width - 1 || $sy >=
$height - 1) {
+ continue;
+ } else {
+ $color = imagecolorat($img, $sx, $sy) & 0xFF;
+ $color_x = imagecolorat($img, $sx + 1, $sy) & 0xFF;
+ $color_y = imagecolorat($img, $sx, $sy + 1) & 0xFF;
+ $color_xy = imagecolorat($img, $sx + 1, $sy + 1) &
0xFF;
+ }
+
+ if ($color == 255 && $color_x == 255 && $color_y == 255 &&
$color_xy == 255) {
+ continue;
+ } elseif ($color == 0 && $color_x == 0 && $color_y == 0 &&
$color_xy == 0) {
+ $newred = $foreground_color[0];
+ $newgreen = $foreground_color[1];
+ $newblue = $foreground_color[2];
+ } else {
+ $frsx = $sx - floor($sx);
+ $frsy = $sy - floor($sy);
+ $frsx1 = 1 - $frsx;
+ $frsy1 = 1 - $frsy;
+
+ $newcolor = (
+ $color * $frsx1 * $frsy1 +
+ $color_x * $frsx * $frsy1 +
+ $color_y * $frsx1 * $frsy +
+ $color_xy * $frsx * $frsy);
+
+ if ($newcolor > 255) {
+ $newcolor = 255;
+ }
+ $newcolor = $newcolor / 255;
+ $newcolor0 = 1 - $newcolor;
+
+ $newred = $newcolor0 * $foreground_color[0] +
$newcolor * $background_color[0];
+ $newgreen = $newcolor0 * $foreground_color[1] +
$newcolor * $background_color[1];
+ $newblue = $newcolor0 * $foreground_color[2] +
$newcolor * $background_color[2];
+ }
+
+ imagesetpixel($img2, $x, $y, imagecolorallocate($img2,
$newred, $newgreen, $newblue));
+ }
+ }
+
+ $algo = $this->_config['hash_algo'];
+ $file = hash($algo, uniqid(rand(), true));
+ $path = $this->_config['save_path'] . $file . '.jpg';
+
+ if (function_exists('imagejpeg')) {
+ imagejpeg($img2, $path, $jpeg_quality);
+ } elseif (function_exists('imagegif')) {
+ imagegif ($img2, $path);
+ } elseif (function_exists('imagepng')) {
+ imagepng($img2, $path);
+ }
+
+ // set a session flash with the hashed solution
+ $hash = hash($algo, $this->_config['salt'] . $solution);
+ $this->_session->setFlash('key', "$hash:$file");
+
+ return $file . '.jpg';
+ }
+
+ /**
+ *
+ * Checks if a CAPTCHA solution is valid.
+ *
+ * @return bool True if the solution is valid, false otherwise.
+ *
+ */
+ public function isValid($value)
+ {
+ if ($key = $this->_session->getFlash('key')) {
+ // parse session
+ settype($key, 'string');
+ list($solution, $file) = explode(':', $key . ':');
+
+ // hash the attempted value
+ $hash = hash($this->_config['hash_algo'],
+ $this->_config['salt'] . $value);
+
+ // valid solution?
+ if ($solution == $hash) {
+ // valid: sanitize file name and delete file
+ $file = preg_replace('/[^a-z0-9]/i', '', $file);
+ $path = $this->_config['save_path'] . $file . '.jpg';
+ if (file_exists($path)) {
+ unlink($path);
+ }
+
+ // done
+ return true;
+ }
+ }
+
+ // invalid solution
+ return false;
+ }
+
+ /**
+ *
+ * Generates a captcha solution.
+ *
+ * @return string A random solution.
+ *
+ */
+ protected function _getSolution()
+ {
+ $solution = '';
+
+ $chars = $this->_config['chars'];
+ $last_key = strlen($chars) - 1;
+ $length = $this->_config['length'];
+
+ while ($length--) {
+ $key = mt_rand(0, $last_key);
+ $solution .= $chars{$key};
+ }
+
+ return $solution;
+ }
+}
Added: trunk/Tipos/Filter/ValidateCaptcha.php
==============================================================================
--- (empty file)
+++ trunk/Tipos/Filter/ValidateCaptcha.php Sun Oct 5 13:40:52 2008
@@ -0,0 +1,38 @@
+<?php
+/**
+ *
+ * Validates a CAPTCHA.
+ *
+ * @category Tipos
+ *
+ * @package Tipos_Filter
+ *
+ * @author Rodrigo Moraes <rodrigo...@gmail.com>
+ *
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ *
+ * @version $Id: ValidateCaptcha.php 1538 2008-02-22 11:28:56Z
rodrigo.moraes $
+ *
+ */
+class Tipos_Filter_ValidateCaptcha extends Solar_Filter_Abstract
+{
+ /**
+ *
+ * Validates a CAPTCHA.
+ *
+ * @param mixed $value The value to validate.
+ *
+ * @return bool True if valid, false if not.
+ *
+ */
+ public function validateCaptcha($value)
+ {
+ if (! $this->_filter->getRequire() && $value === null) {
+ die('meh, should be required!');
+ return true;
+ }
+
+ $captcha = Solar::factory('Tipos_Captcha_Image');
+ return $captcha->isValid($value);
+ }
+}
\ No newline at end of file
Added: trunk/Tipos/View/Helper/FormCaptcha.php
==============================================================================
--- (empty file)
+++ trunk/Tipos/View/Helper/FormCaptcha.php Sun Oct 5 13:40:52 2008
@@ -0,0 +1,56 @@
+<?php
+/**
+ *
+ * Helper to generate a form field for a CAPTCHA image.
+ *
+ * @category Tipos
+ *
+ * @package Tipos_View_Helper
+ *
+ * @author Rodrigo Moraes <rodrigo...@gmail.com>
+ *
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ *
+ * @version $Id: FormCaptcha.php 1537 2008-02-22 11:27:12Z rodrigo.moraes $
+ *
+ */
+class Tipos_View_Helper_FormCaptcha extends Solar_View_Helper_FormElement
+{
+ /**
+ *
+ *
+ *
+ *
+ */
+ protected $_Tipos_View_Helper_FormCaptcha = array(
+ 'path' => null,
+ );
+
+ /**
+ *
+ * Generates a form field for a CAPTCHA image.
+ *
+ * @param array $info An array of element information.
+ *
+ * @return string The element XHTML.
+ *
+ */
+ public function formCaptcha($info)
+ {
+ $this->_prepare($info);
+
+ // save captcha image and solution and get the file name
+ $captcha = Solar::factory('Tipos_Captcha_Image');
+ $file = $captcha->getCaptcha();
+
+ $image = $this->_view->image($this->_config['path'] . $file, array(
+ 'alt' => 'captcha',
+ 'title' => 'captcha'
+ ));
+
+ $out = '<p class="captcha-image">' . $image . '</p>';
+ $out .= $this->_view->formText($info);
+
+ return $out;
+ }
+}