Merge remote-tracking branch 'remotes/jeremy/urlgeneration'

Conflicts:
	DependencyInjection/GregwarCaptchaExtension.php
This commit is contained in:
Gregwar 2012-12-04 10:51:08 +01:00
commit c1b702566b
9 changed files with 392 additions and 261 deletions

View File

@ -0,0 +1,37 @@
<?php
namespace Gregwar\CaptchaBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
/**
* Generates a captcha via a URL
*
* @author Jeremy Livingston <jeremy.j.livingston@gmail.com>
*/
class CaptchaController extends Controller
{
/**
* Action that is used to generate the captcha, save its code, and stream the image
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @param string $key
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function generateCaptchaAction(Request $request, $key)
{
$options = $this->container->getParameter('gregwar_captcha.config');
if (!$options['as_url'] || !in_array($key, $options['valid_keys'])) {
return $this->createNotFoundException('Unable to generate a captcha via a URL without the proper configuration.');
}
/* @var \Gregwar\CaptchaBundle\Generator\CaptchaGenerator $generator */
$generator = $this->container->get('gregwar_captcha.generator');
return new Response($generator->generate($key, $options));
}
}

View File

@ -9,12 +9,13 @@ class Configuration implements ConfigurationInterface
{ {
/** /**
* Generates the configuration tree. * Generates the configuration tree.
*
* @return TreeBuilder * @return TreeBuilder
*/ */
public function getConfigTreeBuilder() public function getConfigTreeBuilder()
{ {
$treeBuilder = new TreeBuilder(); $treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('gregwar_captcha', 'array'); $rootNode = $treeBuilder->root('gregwar_captcha');
$rootNode $rootNode
->addDefaultsIfNotSet() ->addDefaultsIfNotSet()
@ -26,6 +27,7 @@ class Configuration implements ConfigurationInterface
->scalarNode('keep_value')->defaultValue(true)->end() ->scalarNode('keep_value')->defaultValue(true)->end()
->scalarNode('charset')->defaultValue('abcdefhjkmnprstuvwxyz23456789')->end() ->scalarNode('charset')->defaultValue('abcdefhjkmnprstuvwxyz23456789')->end()
->scalarNode('as_file')->defaultValue(false)->end() ->scalarNode('as_file')->defaultValue(false)->end()
->scalarNode('as_url')->defaultValue(false)->end()
->scalarNode('image_folder')->defaultValue('captcha')->end() ->scalarNode('image_folder')->defaultValue('captcha')->end()
->scalarNode('web_path')->defaultValue('%kernel.root_dir%/../web')->end() ->scalarNode('web_path')->defaultValue('%kernel.root_dir%/../web')->end()
->scalarNode('gc_freq')->defaultValue(100)->end() ->scalarNode('gc_freq')->defaultValue(100)->end()
@ -33,8 +35,10 @@ class Configuration implements ConfigurationInterface
->scalarNode('quality')->defaultValue(15)->end() ->scalarNode('quality')->defaultValue(15)->end()
->scalarNode('invalid_message')->defaultValue('Bad code value')->end() ->scalarNode('invalid_message')->defaultValue('Bad code value')->end()
->scalarNode('bypass_code')->defaultValue(null)->end() ->scalarNode('bypass_code')->defaultValue(null)->end()
->arrayNode('valid_keys')->defaultValue(array('captcha'))->prototype('scalar')->end()
->end() ->end()
; ;
return $treeBuilder; return $treeBuilder;
} }
} }

View File

@ -4,12 +4,20 @@ namespace Gregwar\CaptchaBundle\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\FileLocator;
/**
* Extension used to load the configuration, set parameters, and initialize the captcha view
*
* @author Gregwar <g.passault@gmail.com>
*/
class GregwarCaptchaExtension extends Extension class GregwarCaptchaExtension extends Extension
{ {
/**
* @param array $configs
* @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
*/
public function load(array $configs, ContainerBuilder $container) public function load(array $configs, ContainerBuilder $container)
{ {
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
@ -19,6 +27,10 @@ class GregwarCaptchaExtension extends Extension
$config = $this->processConfiguration($configuration, $configs); $config = $this->processConfiguration($configuration, $configs);
$container->setParameter('gregwar_captcha.config', $config); $container->setParameter('gregwar_captcha.config', $config);
$container->setParameter('gregwar_captcha.config.image_folder', $config['image_folder']);
$container->setParameter('gregwar_captcha.config.web_path', $config['web_path']);
$container->setParameter('gregwar_captcha.config.gc_freq', $config['gc_freq']);
$container->setParameter('gregwar_captcha.config.expiration', $config['expiration']);
$resources = $container->getParameter('twig.form.resources'); $resources = $container->getParameter('twig.form.resources');
$container->setParameter('twig.form.resources', array_merge(array('GregwarCaptchaBundle::captcha.html.twig'), $resources)); $container->setParameter('twig.form.resources', array_merge(array('GregwarCaptchaBundle::captcha.html.twig'), $resources));

View File

@ -3,159 +3,124 @@
namespace Gregwar\CaptchaBundle\Generator; namespace Gregwar\CaptchaBundle\Generator;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\RouterInterface;
/** /**
* Generates a CAPTCHA image * Generates a CAPTCHA image
*/ */
class CaptchaGenerator { class CaptchaGenerator
{
/**
* @var \Symfony\Component\HttpFoundation\Session\SessionInterface
*/
protected $session;
/**
* @var \Symfony\Component\Routing\RouterInterface
*/
protected $router;
/** /**
* Name of folder for captcha images * Name of folder for captcha images
* @var string * @var string
*/ */
public $imageFolder; protected $imageFolder;
/** /**
* Absolute path to public web folder * Absolute path to public web folder
* @var string * @var string
*/ */
public $webPath; protected $webPath;
/** /**
* Frequence of garbage collection in fractions of 1 * Frequency of garbage collection in fractions of 1
* @var int * @var int
*/ */
public $gcFreq; protected $gcFreq;
/**
* Captcha Font
* @var string
*/
public $font;
/** /**
* Maximum age of images in minutes * Maximum age of images in minutes
* @var int * @var int
*/ */
public $expiration; protected $expiration;
/** /**
* Random fingerprint * The fingerprint used to generate the image details across requests
* Useful to be able to regenerate exactly the same image * @var array|null
* @var array
*/ */
public $fingerprint; protected $fingerprint;
/** /**
* Should fingerprint be used ? * Whether this instance should use the fingerprint
* @var boolean * @var bool
*/ */
public $use_fingerprint; protected $useFingerprint;
/** /**
* The captcha code * @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
* @var string * @param \Symfony\Component\Routing\RouterInterface $router
* @param string $imageFolder
* @param string $webPath
* @param int $gcFreq
* @param int $expiration
*/ */
public $value; public function __construct(SessionInterface $session, RouterInterface $router, $imageFolder, $webPath, $gcFreq, $expiration)
/**
* Captcha quality
* @var int
*/
public $quality;
public function __construct($value, $imageFolder, $webPath, $gcFreq, $expiration, $font, $fingerprint, $quality)
{ {
$this->value = $value; $this->session = $session;
$this->router = $router;
$this->imageFolder = $imageFolder; $this->imageFolder = $imageFolder;
$this->webPath = $webPath; $this->webPath = $webPath;
$this->gcFreq = intval($gcFreq); $this->gcFreq = $gcFreq;
$this->expiration = intval($expiration); $this->expiration = $expiration;
$this->font = $font;
$this->fingerprint = $fingerprint;
$this->use_fingerprint = (bool)$fingerprint;
$this->quality = intval($quality);
} }
/** /**
* Get the captcha embeded code * Get the captcha URL, stream, or filename that will go in the image's src attribute
*/
public function getCode($width = 120, $height = 40)
{
return 'data:image/jpeg;base64,'.base64_encode($this->generate($width, $height));
}
/**
* Creates a captcha image with provided dimensions
* and randomly executes a garbage collection
* *
* @param int $width * @param $key
* @param int $height * @param array $options
* @return string Web path to the created image *
* @return array
*/ */
public function getFile($width = 120, $height = 40) public function getCaptchaCode($key, array $options)
{ {
// Randomly execute garbage collection and returns the image filename
if ($options['as_file']) {
if (mt_rand(1, $this->gcFreq) == 1) { if (mt_rand(1, $this->gcFreq) == 1) {
$this->garbageCollection(); $this->garbageCollection();
} }
return $this->generate($width, $height, true); return $this->generate($key, $options);
} }
/** // Returns the configured URL for image generation
* Returns a random number or the next number in the if ($options['as_url']) {
* fingerprint return $this->router->generate('gregwar_captcha.generate_captcha', array('key' => $key));
*/
public function rand($min, $max)
{
if (!is_array($this->fingerprint)) {
$this->fingerprint = array();
} }
if ($this->use_fingerprint) { return 'data:image/jpeg;base64,' . base64_encode($this->generate($key, $options));
$value = current($this->fingerprint);
next($this->fingerprint);
} else {
$value = mt_rand($min, $max);
$this->fingerprint[] = $value;
}
return $value;
}
/**
* Get the CAPTCHA fingerprint
*/
public function getFingerprint()
{
return $this->fingerprint;
}
/**
* Deletes all images in the configured folder
* that are older than 10 minutes
*
* @return void
*/
public function garbageCollection()
{
$finder = new Finder();
$criteria = sprintf('<= now - %s minutes', $this->expiration);
$finder->in($this->webPath . '/' . $this->imageFolder)
->date($criteria);
foreach($finder->files() as $file)
{
unlink($file->getPathname());
}
} }
/** /**
* Generate the image * Generate the image
*/ */
public function generate($width, $height, $createFile = false) public function generate($key, array $options)
{ {
$i = imagecreatetruecolor($width,$height); $width = $options['width'];
$height = $options['height'];
if ($options['keep_value'] && $this->session->has($key . '_fingerprint')) {
$this->fingerprint = $this->session->get($key . '_fingerprint');
$this->useFingerprint = true;
} else {
$this->fingerprint = null;
$this->useFingerprint = false;
}
$captchaValue = $this->getCaptchaValue($key, $options['keep_value'], $options['charset'], $options['length']);
$i = imagecreatetruecolor($width,$height);
$col = imagecolorallocate($i, $this->rand(0,110), $this->rand(0,110), $this->rand(0,110)); $col = imagecolorallocate($i, $this->rand(0,110), $this->rand(0,110), $this->rand(0,110));
imagefill($i, 0, 0, 0xFFFFFF); imagefill($i, 0, 0, 0xFFFFFF);
@ -171,86 +136,143 @@ class CaptchaGenerator {
} }
// Write CAPTCHA text // Write CAPTCHA text
$size = $width/strlen($this->value); $size = $width / strlen($captchaValue);
$font = $this->font; $font = $options['font'];
$box = imagettfbbox($size, 0, $font, $this->value); $box = imagettfbbox($size, 0, $font, $captchaValue);
$txt_width = $box[2] - $box[0]; $textWidth = $box[2] - $box[0];
$txt_height = $box[1] - $box[7]; $textHeight = $box[1] - $box[7];
imagettftext($i, $size, 0, ($width-$txt_width)/2, ($height-$txt_height)/2+$size, $col, $font, $this->value); imagettftext($i, $size, 0, ($width - $textWidth) / 2, ($height - $textHeight) / 2 + $size, $col, $font, $captchaValue);
// Distort the image // Distort the image
$X = $this->rand(0, $width); $X = $this->rand(0, $width);
$Y = $this->rand(0, $height); $Y = $this->rand(0, $height);
$Phase=$this->rand(0,10); $phase = $this->rand(0, 10);
$Scale = 1.3 + $this->rand(0,10000)/30000; $scale = 1.3 + $this->rand(0, 10000) / 30000;
$Amp=1+$this->rand(0,1000)/1000;
$out = imagecreatetruecolor($width, $height); $out = imagecreatetruecolor($width, $height);
for ($x=0; $x<$width; $x++) for ($x = 0; $x < $width; $x++) {
for ($y=0; $y<$height; $y++) { for ($y = 0; $y < $height; $y++) {
$Vx=$x-$X; $Vx = $x - $X;
$Vy=$y-$Y; $Vy = $y - $Y;
$Vn=sqrt($Vx*$Vx+$Vy*$Vy); $Vn = sqrt($Vx * $Vx + $Vy * $Vy);
if ($Vn!=0) { if ($Vn != 0) {
$Vn2=$Vn+4*sin($Vn/8); $Vn2 = $Vn + 4 * sin($Vn / 8);
$nX=$X+($Vx*$Vn2/$Vn); $nX = $X + ($Vx * $Vn2 / $Vn);
$nY=$Y+($Vy*$Vn2/$Vn); $nY = $Y + ($Vy * $Vn2 / $Vn);
} else { } else {
$nX=$X; $nX = $X;
$nY=$Y; $nY = $Y;
} }
$nY = $nY+$Scale*sin($Phase + $nX*0.2); $nY = $nY + $scale * sin($phase + $nX * 0.2);
$p = $this->bilinearInterpolate($nX-floor($nX), $nY-floor($nY), $p = $this->bilinearInterpolate($nX - floor($nX), $nY - floor($nY),
$this->getCol($i,floor($nX),floor($nY)), $this->getCol($i, floor($nX), floor($nY)),
$this->getCol($i,ceil($nX),floor($nY)), $this->getCol($i, ceil($nX), floor($nY)),
$this->getCol($i,floor($nX),ceil($nY)), $this->getCol($i, floor($nX), ceil($nY)),
$this->getCol($i,ceil($nX),ceil($nY))); $this->getCol($i, ceil($nX), ceil($nY)));
if ($p==0) { if ($p == 0) {
$p=0xFFFFFF; $p = 0xFFFFFF;
} }
imagesetpixel($out, $x, $y, $p); imagesetpixel($out, $x, $y, $p);
} }
}
if ($options['keep_value']) {
$this->session->set($key . '_fingerprint', $this->fingerprint);
}
// Renders it // Renders it
if (!$createFile) { if (!$options['as_file']) {
ob_start(); ob_start();
imagejpeg($out, null, $this->quality); imagejpeg($out, null, $options['quality']);
return ob_get_clean(); return ob_get_clean();
} else { }
// Check if folder exists and create it if not // Check if folder exists and create it if not
if (!file_exists($this->webPath . '/' . $this->imageFolder)) { if (!file_exists($this->webPath . '/' . $this->imageFolder)) {
mkdir($this->webPath . '/' . $this->imageFolder, 0755); mkdir($this->webPath . '/' . $this->imageFolder, 0755);
} }
$filename = md5(uniqid()) . '.jpg'; $filename = md5(uniqid()) . '.jpg';
$filepath = $this->webPath . '/' . $this->imageFolder . '/' . $filename; $filepath = $this->webPath . '/' . $this->imageFolder . '/' . $filename;
imagejpeg($out, $filepath, 15); imagejpeg($out, $filepath, 15);
return '/' . $this->imageFolder . '/' . $filename; return '/' . $this->imageFolder . '/' . $filename;
} }
}
protected function getCol($image, $x, $y) /**
* Generate a new captcha value or pull the existing one from the session
*
* @param string $key
* @param bool $keepValue
* @param string $charset
* @param int $length
*
* @return mixed|string
*/
protected function getCaptchaValue($key, $keepValue, $charset, $length)
{ {
$L = imagesx($image); if ($keepValue && $this->session->has($key)) {
$H = imagesy($image); return $this->session->get($key);
if ($x<0 || $x>=$L || $y<0 || $y>=$H)
return 0xFFFFFF;
else return imagecolorat($image, $x, $y);
} }
protected function getRGB($col) { $value = '';
return array( $chars = str_split($charset);
(int)($col >> 16) & 0xff,
(int)($col >> 8) & 0xff, for ($i=0; $i < $length; $i++) {
(int)($col) & 0xff, $value .= $chars[array_rand($chars)];
);
} }
function bilinearInterpolate($x, $y, $nw, $ne, $sw, $se) $this->session->set($key, $value);
return $value;
}
/**
* Deletes all images in the configured folder
* that are older than the configured number of minutes
*
* @return void
*/
protected function garbageCollection()
{
$finder = new Finder();
$criteria = sprintf('<= now - %s minutes', $this->expiration);
$finder->in($this->webPath . '/' . $this->imageFolder)
->date($criteria);
foreach($finder->files() as $file) {
unlink($file->getPathname());
}
}
/**
* Returns a random number or the next number in the
* fingerprint
*/
protected function rand($min, $max)
{
if (!is_array($this->fingerprint)) {
$this->fingerprint = array();
}
if ($this->useFingerprint) {
$value = current($this->fingerprint);
next($this->fingerprint);
} else {
$value = mt_rand($min, $max);
$this->fingerprint[] = $value;
}
return $value;
}
protected function bilinearInterpolate($x, $y, $nw, $ne, $sw, $se)
{ {
list($r0, $g0, $b0) = $this->getRGB($nw); list($r0, $g0, $b0) = $this->getRGB($nw);
list($r1, $g1, $b1) = $this->getRGB($ne); list($r1, $g1, $b1) = $this->getRGB($ne);
@ -274,5 +296,24 @@ class CaptchaGenerator {
return ($r << 16) | ($g << 8) | $b; return ($r << 16) | ($g << 8) | $b;
} }
protected function getCol($image, $x, $y)
{
$L = imagesx($image);
$H = imagesy($image);
if ($x < 0 || $x >= $L || $y < 0 || $y >= $H) {
return 0xFFFFFF;
}
return imagecolorat($image, $x, $y);
}
protected function getRGB($col) {
return array(
(int)($col >> 16) & 0xff,
(int)($col >> 8) & 0xff,
(int)($col) & 0xff,
);
}
} }

View File

@ -106,13 +106,20 @@ You can use the "captcha" type in your forms this way:
// ... // ...
``` ```
Note that the generated image will be embeded in the HTML document, to avoid dealing Note that the generated image will, by default, be embedded in the HTML document
with route and subrequests. to avoid dealing with route and subrequests.
Options Options
======= =======
You can define the following type option : You can define the following configuration options globally:
* **image_folder**: name of folder for captcha images relative to public web folder in case **as_file** is set to true (default="captcha")
* **web_path**: absolute path to public web folder (default="%kernel.root_dir%/../web")
* **gc_freq**: frequency of garbage collection in fractions of 1 (default=100)
* **expiration**: maximum lifetime of captcha image files in minutes (default=60)
You can define the following configuration options globally or on the CaptchaType itself:
* **width**: the width of the captcha image (default=120) * **width**: the width of the captcha image (default=120)
* **height**: the height of the captcha image (default=40) * **height**: the height of the captcha image (default=40)
@ -122,12 +129,10 @@ You can define the following type option :
* **font**: the font to use (default=Generator/Font/captcha.ttf) * **font**: the font to use (default=Generator/Font/captcha.ttf)
* **keep_value**: the value will be the same until the form is posted, even if the page is refreshed (default=true) * **keep_value**: the value will be the same until the form is posted, even if the page is refreshed (default=true)
* **as_file**: if set to true an image file will be created instead of embedding to please IE6/7 (default=false) * **as_file**: if set to true an image file will be created instead of embedding to please IE6/7 (default=false)
* **image_folder**: name of folder for captcha images relative to public web folder in case **as_file** ist set to true (default="captcha") * **as_url**: if set to true, a URL will be used in the image tag and will handle captcha generation. This can be used in a multiple-server environment and support IE6/7 (default=false)
* **web_path**: absolute path to public web folder (default="%kernel.root_dir%/../web")
* **gc_freq**: frequency of garbage collection in fractions of 1 (default=100)
* **expiration**: maximum lifetime of captcha image files in minutes (default=60)
* **invalid_message**: error message displayed when an non-matching code is submitted (default="Bad code value") * **invalid_message**: error message displayed when an non-matching code is submitted (default="Bad code value")
* **bypass_code**: code that will always validate the captcha (default=null) * **bypass_code**: code that will always validate the captcha (default=null)
* **valid_keys**: names that are able to be used for a captcha form type (default=[captcha])
Example : Example :
@ -149,10 +154,28 @@ configuration entry in your `config.yml` file:
height: 50 height: 50
length: 6 length: 6
Form theming As URL
============
To use a URL to generate a captcha image, you must add the bundle's routing configuration to your app/routing.yml file:
gregwar_captcha_routing:
resource: "@GregwarCaptchaBundle/Resources/config/routing/routing.yml"
This will use the bundle's route of "/generate-captcha/{key}" to handle the generation. If this route conflicts with an application route, you can prefix the bundle's routes when you import:
gregwar_captcha_routing:
resource: "@GregwarCaptchaBundle/Resources/config/routing/routing.yml"
prefix: /_gcb
If you are using multiple captchas or assigning names other than the default "captcha", you will need to whitelist your captcha names in the "valid_keys" configuration:
gregwar_captcha:
valid_keys: [registration_captcha, confirmation_captcha]
Form Theming
============ ============
The widget support the standard symfony theming, see the [documentation](http://symfony.com/doc/current/book/forms.html#form-theming) for details on how to accomplish this. The widget support the standard Symfony theming, see the [documentation](http://symfony.com/doc/current/book/forms.html#form-theming) for details on how to accomplish this.
The default rendering is: The default rendering is:

View File

@ -0,0 +1,3 @@
gregwar_captcha.generate_captcha:
pattern: /generate-captcha/{key}
defaults: { _controller: GregwarCaptchaBundle:Captcha:generateCaptcha }

View File

@ -3,6 +3,19 @@ services:
# captcha type # captcha type
captcha.type: captcha.type:
class: Gregwar\CaptchaBundle\Type\CaptchaType class: Gregwar\CaptchaBundle\Type\CaptchaType
arguments: [ "@session", %gregwar_captcha.config% ] arguments:
- @session
- @gregwar_captcha.generator
- %gregwar_captcha.config%
tags: tags:
- { name: form.type, alias: captcha } - { name: form.type, alias: captcha }
gregwar_captcha.generator:
class: Gregwar\CaptchaBundle\Generator\CaptchaGenerator
arguments:
- @session
- @router
- %gregwar_captcha.config.image_folder%
- %gregwar_captcha.config.web_path%
- %gregwar_captcha.config.gc_freq%
- %gregwar_captcha.config.expiration%

View File

@ -2,20 +2,16 @@
namespace Gregwar\CaptchaBundle\Type; namespace Gregwar\CaptchaBundle\Type;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Form\FormView; use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormViewInterface; use Symfony\Component\Form\FormEvents;
use Gregwar\CaptchaBundle\Validator\CaptchaValidator; use Gregwar\CaptchaBundle\Validator\CaptchaValidator;
use Gregwar\CaptchaBundle\Generator\CaptchaGenerator; use Gregwar\CaptchaBundle\Generator\CaptchaGenerator;
use Gregwar\CaptchaBundle\DataTransformer\EmptyTransformer;
/** /**
* Captcha type * Captcha type
@ -24,6 +20,16 @@ use Gregwar\CaptchaBundle\DataTransformer\EmptyTransformer;
*/ */
class CaptchaType extends AbstractType class CaptchaType extends AbstractType
{ {
/**
* @var \Symfony\Component\HttpFoundation\Session\SessionInterface
*/
protected $session;
/**
* @var \Gregwar\CaptchaBundle\Generator\CaptchaGenerator
*/
protected $generator;
/** /**
* Options * Options
* @var array * @var array
@ -31,99 +37,70 @@ class CaptchaType extends AbstractType
private $options = array(); private $options = array();
/** /**
* Session key * @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
* @var string * @param \Gregwar\CaptchaBundle\Generator\CaptchaGenerator $generator
* @param array $options
*/ */
private $key = 'captcha'; public function __construct(SessionInterface $session, CaptchaGenerator $generator, $options)
public function __construct(Session $session, $config)
{ {
$this->session = $session; $this->session = $session;
$this->options = $config; $this->generator = $generator;
$this->options = $options;
} }
/**
* @param \Symfony\Component\Form\FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
{ {
$this->key = $builder->getForm()->getName(); $validator = new CaptchaValidator(
$this->session,
$builder->addValidator( $builder->getForm()->getName(),
new CaptchaValidator($this->session,
$this->key,
$options['invalid_message'], $options['invalid_message'],
$options['bypass_code']) $options['bypass_code']
); );
$builder->addEventListener(FormEvents::POST_BIND, array($validator, 'validate'));
} }
/**
* @param \Symfony\Component\Form\FormView $view
* @param \Symfony\Component\Form\FormInterface $form
* @param array $options
*/
public function buildView(FormView $view, FormInterface $form, array $options) public function buildView(FormView $view, FormInterface $form, array $options)
{ {
$fingerprint = null; $view->vars = array_merge($view->vars, array(
if ($options['keep_value'] && $this->session->has($this->key.'_fingerprint')) {
$fingerprint = $this->session->get($this->key.'_fingerprint');
}
$generator = new CaptchaGenerator($this->generateCaptchaValue(),
$options['image_folder'],
$options['web_path'],
$options['gc_freq'],
$options['expiration'],
$options['font'],
$fingerprint,
$options['quality']);
if ($options['as_file']) {
$captchaCode = $generator->getFile($options['width'], $options['height']);
} else {
$captchaCode = $generator->getCode($options['width'], $options['height']);
}
if ($options['keep_value']) {
$this->session->set($this->key.'_fingerprint', $generator->getFingerprint());
}
$fieldVars = array(
'captcha_width' => $options['width'], 'captcha_width' => $options['width'],
'captcha_height' => $options['height'], 'captcha_height' => $options['height'],
'captcha_code' => $captchaCode, 'captcha_code' => $this->generator->getCaptchaCode($form->getName(), $options),
'value' => '', 'value' => '',
); ));
foreach($fieldVars as $name => $value){
$view->set($name,$value);
}
} }
/**
* @param \Symfony\Component\OptionsResolver\OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver) public function setDefaultOptions(OptionsResolverInterface $resolver)
{ {
$this->options['property_path'] = false; $this->options['property_path'] = false;
$resolver->setDefaults($this->options); $resolver->setDefaults($this->options);
} }
/**
* @return string
*/
public function getParent() public function getParent()
{ {
return 'text'; return 'text';
} }
/**
* @return string
*/
public function getName() public function getName()
{ {
return 'captcha'; return 'captcha';
} }
private function generateCaptchaValue()
{
if (!$this->options['keep_value'] || !$this->session->has($this->key)) {
$value = '';
$chars = str_split($this->options['charset']);
for ($i=0; $i<$this->options['length']; $i++) {
$value.= $chars[array_rand($chars)];
}
$this->session->set($this->key, $value);
} else {
$value = $this->session->get($this->key);
}
return $value;
}
} }

View File

@ -2,20 +2,19 @@
namespace Gregwar\CaptchaBundle\Validator; namespace Gregwar\CaptchaBundle\Validator;
use Symfony\Component\Form\FormValidatorInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
/** /**
* Captcha validator * Captcha validator
* *
* @author Gregwar <g.passault@gmail.com> * @author Gregwar <g.passault@gmail.com>
*/ */
class CaptchaValidator implements FormValidatorInterface class CaptchaValidator
{ {
/** /**
* Session * @var \Symfony\Component\HttpFoundation\Session\SessionInterface
*/ */
private $session; private $session;
@ -34,7 +33,13 @@ class CaptchaValidator implements FormValidatorInterface
*/ */
private $bypassCode; private $bypassCode;
public function __construct(Session $session, $key, $invalidMessage, $bypassCode) /**
* @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
* @param string $key
* @param string $invalidMessage
* @param string|null $bypassCode
*/
public function __construct(SessionInterface $session, $key, $invalidMessage, $bypassCode)
{ {
$this->session = $session; $this->session = $session;
$this->key = $key; $this->key = $key;
@ -42,46 +47,62 @@ class CaptchaValidator implements FormValidatorInterface
$this->bypassCode = $bypassCode; $this->bypassCode = $bypassCode;
} }
public function validate(FormInterface $form) /**
* @param FormEvent $event
*/
public function validate(FormEvent $event)
{ {
$form = $form = $event->getForm();
$code = $form->getData(); $code = $form->getData();
$expectedCode = $this->getExpectedCode(); $expectedCode = $this->getExpectedCode();
if (!($code && is_string($code) if (!($code && is_string($code) && ($this->compare($code, $expectedCode) || $this->compare($code, $this->bypassCode)))) {
&& ($this->compare($code, $expectedCode) || $this->compare($code, $this->bypassCode)))) {
$form->addError(new FormError($this->invalidMessage)); $form->addError(new FormError($this->invalidMessage));
} }
$this->session->remove($this->key); $this->session->remove($this->key);
if ($this->session->has($this->key.'_fingerprint')) { if ($this->session->has($this->key . '_fingerprint')) {
$this->session->remove($this->key.'_fingerprint'); $this->session->remove($this->key . '_fingerprint');
} }
} }
/** /**
* Retrieve the expected CAPTCHA code * Retrieve the expected CAPTCHA code
*
* @return mixed|null
*/ */
private function getExpectedCode() protected function getExpectedCode()
{ {
if ($this->session->has($this->key)) { if ($this->session->has($this->key)) {
return $this->session->get($this->key); return $this->session->get($this->key);
} }
return null; return null;
} }
/** /**
* Process the codes * Process the codes
*
* @param $code
*
* @return string
*/ */
private function niceize($code) protected function niceize($code)
{ {
return strtr(strtolower($code), 'oil', '01l'); return strtr(strtolower($code), 'oil', '01l');
} }
/** /**
* Run a match comparison on the provided code and the expected code * Run a match comparison on the provided code and the expected code
*
* @param $code
* @param $expectedCode
*
* @return bool
*/ */
private function compare($code, $expectedCode) protected function compare($code, $expectedCode)
{ {
return ($expectedCode && is_string($expectedCode) && $this->niceize($code) == $this->niceize($expectedCode)); return ($expectedCode && is_string($expectedCode) && $this->niceize($code) == $this->niceize($expectedCode));
} }