Add URL generation method and update to Symfony 2.1 standards

This commit is contained in:
Jeremy Livingston 2012-11-13 22:33:36 -05:00
parent 78e1cee035
commit a41e4dd865
9 changed files with 348 additions and 229 deletions

View File

@ -0,0 +1,35 @@
<?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
* @return \Symfony\Component\HttpFoundation\Response
*/
public function generateCaptchaAction(Request $request)
{
$options = $this->container->getParameter('gregwar_captcha.config');
if (!$options['as_url']) {
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($options));
}
}

View File

@ -9,6 +9,7 @@ class Configuration implements ConfigurationInterface
{
/**
* Generates the configuration tree.
*
* @return TreeBuilder
*/
public function getConfigTreeBuilder()
@ -26,6 +27,8 @@ class Configuration implements ConfigurationInterface
->scalarNode('keep_value')->defaultValue(true)->end()
->scalarNode('charset')->defaultValue('abcdefhjkmnprstuvwxyz23456789')->end()
->scalarNode('as_file')->defaultValue(false)->end()
->scalarNode('as_url')->defaultValue(false)->end()
->scalarNode('url')->defaultValue('/generate-captcha')->end()
->scalarNode('image_folder')->defaultValue('captcha')->end()
->scalarNode('web_path')->defaultValue('%kernel.root_dir%/../web')->end()
->scalarNode('gc_freq')->defaultValue(100)->end()

View File

@ -4,15 +4,22 @@ namespace Gregwar\CaptchaBundle\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
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
{
/**
* @param array $configs
* @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
*/
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
@ -20,11 +27,13 @@ class GregwarCaptchaExtension extends Extension
$config = $this->processConfiguration($configuration, $configs);
$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']);
$container->setParameter('gregwar_captcha.config.url', $config['url']);
$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,126 @@
namespace Gregwar\CaptchaBundle\Generator;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* Generates a CAPTCHA image
*/
class CaptchaGenerator {
class CaptchaGenerator
{
/**
* @var \Symfony\Component\HttpFoundation\Session\SessionInterface
*/
protected $session;
/**
* Name of folder for captcha images
* @var string
*/
public $imageFolder;
protected $imageFolder;
/**
* Absolute path to public web folder
* @var string
*/
public $webPath;
protected $webPath;
/**
* Frequence of garbage collection in fractions of 1
* Frequency of garbage collection in fractions of 1
* @var int
*/
public $gcFreq;
/**
* Captcha Font
* @var string
*/
public $font;
protected $gcFreq;
/**
* Maximum age of images in minutes
* @var int
*/
public $expiration;
protected $expiration;
/**
* Random fingerprint
* Useful to be able to regenerate exactly the same image
* @var array
* The fingerprint used to generate the image details across requests
* @var array|null
*/
public $fingerprint;
protected $fingerprint;
/**
* Should fingerprint be used ?
* @var boolean
* Whether this instance should use the fingerprint
* @var bool
*/
public $use_fingerprint;
protected $useFingerprint;
/**
* The captcha code
* The key used to store the value to the session
* @var string
*/
public $value;
protected $key = 'captcha';
/**
* Captcha quality
* @var int
* @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
* @param string $imageFolder
* @param string $webPath
* @param int $gcFreq
* @param int $expiration
* @param string $url
*/
public $quality;
public function __construct($value, $imageFolder, $webPath, $gcFreq, $expiration, $font, $fingerprint, $quality)
public function __construct(SessionInterface $session, $imageFolder, $webPath, $gcFreq, $expiration, $url)
{
$this->value = $value;
$this->session = $session;
$this->imageFolder = $imageFolder;
$this->webPath = $webPath;
$this->gcFreq = intval($gcFreq);
$this->expiration = intval($expiration);
$this->font = $font;
$this->fingerprint = $fingerprint;
$this->use_fingerprint = (bool)$fingerprint;
$this->quality = intval($quality);
$this->gcFreq = $gcFreq;
$this->expiration = $expiration;
$this->url = $url;
}
/**
* Get the captcha embeded code
*/
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
* Get the captcha URL, stream, or filename that will go in the image's src attribute
*
* @param int $width
* @param int $height
* @return string Web path to the created image
* @param $key
* @param array $options
*
* @return array
*/
public function getFile($width = 120, $height = 40)
public function getCaptchaCode($key, array $options)
{
$this->key = $key;
// Randomly execute garbage collection and returns the image filename
if ($options['as_file']) {
if (mt_rand(1, $this->gcFreq) == 1) {
$this->garbageCollection();
}
return $this->generate($width, $height, true);
return $this->generate($options);
}
/**
* Returns a random number or the next number in the
* fingerprint
*/
public function rand($min, $max)
{
if (!is_array($this->fingerprint)) {
$this->fingerprint = array();
// Returns the configured URL for image generation
if ($options['as_url']) {
return $this->url;
}
if ($this->use_fingerprint) {
$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());
}
return 'data:image/jpeg;base64,' . base64_encode($this->generate($options));
}
/**
* Generate the image
*/
public function generate($width, $height, $createFile = false)
public function generate(array $options)
{
$i = imagecreatetruecolor($width,$height);
$width = $options['width'];
$height = $options['height'];
if ($options['keep_value'] && $this->session->has($this->key.'_fingerprint')) {
$this->fingerprint = $this->session->get($this->key.'_fingerprint');
$this->useFingerprint = true;
} else {
$this->fingerprint = null;
$this->useFingerprint = false;
}
$captchaValue = $this->getCaptchaValue($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));
imagefill($i, 0, 0, 0xFFFFFF);
@ -171,13 +138,13 @@ class CaptchaGenerator {
}
// Write CAPTCHA text
$size = $width/strlen($this->value);
$font = $this->font;
$box = imagettfbbox($size, 0, $font, $this->value);
$size = $width/strlen($captchaValue);
$font = $options['font'];
$box = imagettfbbox($size, 0, $font, $captchaValue);
$txt_width = $box[2] - $box[0];
$txt_height = $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-$txt_width)/2, ($height-$txt_height)/2+$size, $col, $font, $captchaValue);
// Distort the image
$X = $this->rand(0, $width);
@ -187,7 +154,7 @@ class CaptchaGenerator {
$Amp=1+$this->rand(0,1000)/1000;
$out = imagecreatetruecolor($width, $height);
for ($x=0; $x<$width; $x++)
for ($x=0; $x<$width; $x++) {
for ($y=0; $y<$height; $y++) {
$Vx=$x-$X;
$Vy=$y-$Y;
@ -215,42 +182,99 @@ class CaptchaGenerator {
imagesetpixel($out, $x, $y, $p);
}
}
if ($options['keep_value']) {
$this->session->set($this->key.'_fingerprint', $this->fingerprint);
}
// Renders it
if (!$createFile) {
if (!$options['as_file']) {
ob_start();
imagejpeg($out, null, $this->quality);
imagejpeg($out, null, $options['quality']);
return ob_get_clean();
} else {
}
// Check if folder exists and create it if not
if (!file_exists($this->webPath . '/' . $this->imageFolder)) {
mkdir($this->webPath . '/' . $this->imageFolder, 0755);
}
$filename = md5(uniqid()) . '.jpg';
$filepath = $this->webPath . '/' . $this->imageFolder . '/' . $filename;
imagejpeg($out, $filepath, 15);
return '/' . $this->imageFolder . '/' . $filename;
}
}
protected function getCol($image, $x, $y)
/**
* Generate a new captcha value or pull the existing one from the session
*
* @param bool $keepValue
* @param string $charset
* @param int $length
*
* @return mixed|string
*/
protected function getCaptchaValue($keepValue, $charset, $length)
{
$L = imagesx($image);
$H = imagesy($image);
if ($x<0 || $x>=$L || $y<0 || $y>=$H)
return 0xFFFFFF;
else return imagecolorat($image, $x, $y);
if ($keepValue && $this->session->has($this->key)) {
return $this->session->get($this->key);
}
protected function getRGB($col) {
return array(
(int)($col >> 16) & 0xff,
(int)($col >> 8) & 0xff,
(int)($col) & 0xff,
);
$value = '';
$chars = str_split($charset);
for ($i=0; $i < $length; $i++) {
$value .= $chars[array_rand($chars)];
}
function bilinearInterpolate($x, $y, $nw, $ne, $sw, $se)
$this->session->set($this->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($r1, $g1, $b1) = $this->getRGB($ne);
@ -274,5 +298,24 @@ class CaptchaGenerator {
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,21 @@ 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
with route and subrequests.
Note that the generated image will, by default, be embedded in the HTML document
to avoid dealing with route and subrequests.
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)
* **url**: the url to use when **as_url** is set to true (default=/generate-captcha)
You can define the following configuration options globally or on the CaptchaType itself:
* **width**: the width of the captcha image (default=120)
* **height**: the height of the captcha image (default=40)
@ -122,10 +130,7 @@ You can define the following type option :
* **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)
* **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")
* **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)
* **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)
* **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)
@ -149,10 +154,17 @@ configuration entry in your `config.yml` file:
height: 50
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"
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:

View File

@ -0,0 +1,3 @@
gregwar_captcha.generate_captcha:
pattern: %gregwar_captcha.config.url%
defaults: { _controller: GregwarCaptchaBundle:Captcha:generateCaptcha }

View File

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

View File

@ -2,20 +2,16 @@
namespace Gregwar\CaptchaBundle\Type;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormViewInterface;
use Symfony\Component\Form\FormEvents;
use Gregwar\CaptchaBundle\Validator\CaptchaValidator;
use Gregwar\CaptchaBundle\Generator\CaptchaGenerator;
use Gregwar\CaptchaBundle\DataTransformer\EmptyTransformer;
/**
* Captcha type
@ -24,6 +20,16 @@ use Gregwar\CaptchaBundle\DataTransformer\EmptyTransformer;
*/
class CaptchaType extends AbstractType
{
/**
* @var \Symfony\Component\HttpFoundation\Session\SessionInterface
*/
protected $session;
/**
* @var \Gregwar\CaptchaBundle\Generator\CaptchaGenerator
*/
protected $generator;
/**
* Options
* @var array
@ -36,94 +42,67 @@ class CaptchaType extends AbstractType
*/
private $key = 'captcha';
public function __construct(Session $session, $config)
/**
* @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
* @param \Gregwar\CaptchaBundle\Generator\CaptchaGenerator $generator
* @param array $options
*/
public function __construct(SessionInterface $session, CaptchaGenerator $generator, $options)
{
$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)
{
$this->key = $builder->getForm()->getName();
$builder->addValidator(
new CaptchaValidator($this->session,
$this->key,
$options['invalid_message'],
$options['bypass_code'])
);
$validator = new CaptchaValidator($this->session, $this->key, $options['invalid_message'], $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)
{
$fingerprint = null;
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(
$view->vars = array_merge($view->vars, array(
'captcha_width' => $options['width'],
'captcha_height' => $options['height'],
'captcha_code' => $captchaCode,
'captcha_code' => $this->generator->getCaptchaCode($this->key, $options),
'value' => '',
);
foreach($fieldVars as $name => $value){
$view->set($name,$value);
}
));
}
/**
* @param \Symfony\Component\OptionsResolver\OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->options['property_path'] = false;
$resolver->setDefaults($this->options);
}
/**
* @return string
*/
public function getParent()
{
return 'text';
}
/**
* @return string
*/
public function getName()
{
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;
use Symfony\Component\Form\FormValidatorInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
/**
* Captcha validator
*
* @author Gregwar <g.passault@gmail.com>
*/
class CaptchaValidator implements FormValidatorInterface
class CaptchaValidator
{
/**
* Session
* @var \Symfony\Component\HttpFoundation\Session\SessionInterface
*/
private $session;
@ -34,7 +33,13 @@ class CaptchaValidator implements FormValidatorInterface
*/
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->key = $key;
@ -42,8 +47,13 @@ class CaptchaValidator implements FormValidatorInterface
$this->bypassCode = $bypassCode;
}
public function validate(FormInterface $form)
/**
* @param FormEvent $event
*/
public function validate(FormEvent $event)
{
$form = $form = $event->getForm();
$code = $form->getData();
$expectedCode = $this->getExpectedCode();
@ -54,34 +64,46 @@ class CaptchaValidator implements FormValidatorInterface
$this->session->remove($this->key);
if ($this->session->has($this->key.'_fingerprint')) {
$this->session->remove($this->key.'_fingerprint');
if ($this->session->has($this->key . '_fingerprint')) {
$this->session->remove($this->key . '_fingerprint');
}
}
/**
* Retrieve the expected CAPTCHA code
*
* @return mixed|null
*/
private function getExpectedCode()
protected function getExpectedCode()
{
if ($this->session->has($this->key)) {
return $this->session->get($this->key);
}
return null;
}
/**
* Process the codes
*
* @param $code
*
* @return string
*/
private function niceize($code)
protected function niceize($code)
{
return strtr(strtolower($code), 'oil', '01l');
}
/**
* 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));
}