From a41e4dd8652d0e085e6d0534ae26331d1f31bc78 Mon Sep 17 00:00:00 2001 From: Jeremy Livingston Date: Tue, 13 Nov 2012 22:33:36 -0500 Subject: [PATCH 1/6] Add URL generation method and update to Symfony 2.1 standards --- Controller/CaptchaController.php | 35 ++ DependencyInjection/Configuration.php | 3 + .../GregwarCaptchaExtension.php | 23 +- Generator/CaptchaGenerator.php | 305 ++++++++++-------- README.md | 30 +- Resources/config/routing/routing.yml | 3 + Resources/config/services.yml | 15 +- Type/CaptchaType.php | 109 +++---- Validator/CaptchaValidator.php | 54 +++- 9 files changed, 348 insertions(+), 229 deletions(-) create mode 100644 Controller/CaptchaController.php create mode 100644 Resources/config/routing/routing.yml diff --git a/Controller/CaptchaController.php b/Controller/CaptchaController.php new file mode 100644 index 0000000..7600567 --- /dev/null +++ b/Controller/CaptchaController.php @@ -0,0 +1,35 @@ + + */ +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)); + } +} + diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 3b0da0d..e8d15e5 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -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() diff --git a/DependencyInjection/GregwarCaptchaExtension.php b/DependencyInjection/GregwarCaptchaExtension.php index 72f61f8..f1c7209 100755 --- a/DependencyInjection/GregwarCaptchaExtension.php +++ b/DependencyInjection/GregwarCaptchaExtension.php @@ -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 + */ 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)); } - -} - +} \ No newline at end of file diff --git a/Generator/CaptchaGenerator.php b/Generator/CaptchaGenerator.php index 24e6c60..2620c94 100644 --- a/Generator/CaptchaGenerator.php +++ b/Generator/CaptchaGenerator.php @@ -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->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->session = $session; + $this->imageFolder = $imageFolder; + $this->webPath = $webPath; + $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 - */ - public function getFile($width = 120, $height = 40) - { - if (mt_rand(1, $this->gcFreq) == 1) { - $this->garbageCollection(); - } - - return $this->generate($width, $height, true); - } - - /** - * Returns a random number or the next number in the - * fingerprint - */ - public function rand($min, $max) - { - if (!is_array($this->fingerprint)) { - $this->fingerprint = array(); - } - - 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 + * @param $key + * @param array $options * - * @return void + * @return array */ - public function garbageCollection() + public function getCaptchaCode($key, array $options) { - $finder = new Finder(); - $criteria = sprintf('<= now - %s minutes', $this->expiration); - $finder->in($this->webPath . '/' . $this->imageFolder) - ->date($criteria); + $this->key = $key; - foreach($finder->files() as $file) - { - unlink($file->getPathname()); + // 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($options); } + + // Returns the configured URL for image generation + if ($options['as_url']) { + return $this->url; + } + + 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; + } + + // 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; + } + + /** + * 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) + { + if ($keepValue && $this->session->has($this->key)) { + return $this->session->get($this->key); + } + + $value = ''; + $chars = str_split($charset); + + for ($i=0; $i < $length; $i++) { + $value .= $chars[array_rand($chars)]; + } + + $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()); } } - protected function getCol($image, $x, $y) + /** + * Returns a random number or the next number in the + * fingerprint + */ + protected function rand($min, $max) { - $L = imagesx($image); - $H = imagesy($image); - if ($x<0 || $x>=$L || $y<0 || $y>=$H) - return 0xFFFFFF; - else return imagecolorat($image, $x, $y); + 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 getRGB($col) { - return array( - (int)($col >> 16) & 0xff, - (int)($col >> 8) & 0xff, - (int)($col) & 0xff, - ); - } - - function bilinearInterpolate($x, $y, $nw, $ne, $sw, $se) + 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, + ); + } } diff --git a/README.md b/README.md index bdbee91..acc6956 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/Resources/config/routing/routing.yml b/Resources/config/routing/routing.yml new file mode 100644 index 0000000..36126a3 --- /dev/null +++ b/Resources/config/routing/routing.yml @@ -0,0 +1,3 @@ +gregwar_captcha.generate_captcha: + pattern: %gregwar_captcha.config.url% + defaults: { _controller: GregwarCaptchaBundle:Captcha:generateCaptcha } \ No newline at end of file diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 9f91571..3e13e64 100755 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -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% diff --git a/Type/CaptchaType.php b/Type/CaptchaType.php index 92f7824..ba2463b 100644 --- a/Type/CaptchaType.php +++ b/Type/CaptchaType.php @@ -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->session = $session; + $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; - } } \ No newline at end of file diff --git a/Validator/CaptchaValidator.php b/Validator/CaptchaValidator.php index d9a0fb1..166b08b 100644 --- a/Validator/CaptchaValidator.php +++ b/Validator/CaptchaValidator.php @@ -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 */ -class CaptchaValidator implements FormValidatorInterface +class CaptchaValidator { /** - * Session + * @var \Symfony\Component\HttpFoundation\Session\SessionInterface */ private $session; @@ -34,16 +33,27 @@ 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; - $this->invalidMessage = $invalidMessage; - $this->bypassCode = $bypassCode; + $this->session = $session; + $this->key = $key; + $this->invalidMessage = $invalidMessage; + $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)); } From 9fc82c845328da19f0dde83e40018ce19e2e1903 Mon Sep 17 00:00:00 2001 From: Jeremy Livingston Date: Mon, 3 Dec 2012 14:49:17 -0500 Subject: [PATCH 2/6] Add key parameter to URL generation method. --- Controller/CaptchaController.php | 8 ++-- DependencyInjection/Configuration.php | 5 ++- .../GregwarCaptchaExtension.php | 1 - Generator/CaptchaGenerator.php | 45 +++++++++---------- Resources/config/routing/routing.yml | 4 +- Resources/config/services.yml | 4 +- 6 files changed, 34 insertions(+), 33 deletions(-) diff --git a/Controller/CaptchaController.php b/Controller/CaptchaController.php index 7600567..0a1e35e 100644 --- a/Controller/CaptchaController.php +++ b/Controller/CaptchaController.php @@ -17,19 +17,21 @@ 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) + public function generateCaptchaAction(Request $request, $key) { $options = $this->container->getParameter('gregwar_captcha.config'); - if (!$options['as_url']) { + 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($options)); + return new Response($generator->generate($key, $options)); } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index e8d15e5..4cc0b36 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -15,7 +15,7 @@ class Configuration implements ConfigurationInterface public function getConfigTreeBuilder() { $treeBuilder = new TreeBuilder(); - $rootNode = $treeBuilder->root('gregwar_captcha', 'array'); + $rootNode = $treeBuilder->root('gregwar_captcha'); $rootNode ->addDefaultsIfNotSet() @@ -28,7 +28,6 @@ class Configuration implements ConfigurationInterface ->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() @@ -36,8 +35,10 @@ class Configuration implements ConfigurationInterface ->scalarNode('quality')->defaultValue(15)->end() ->scalarNode('invalid_message')->defaultValue('Bad code value')->end() ->scalarNode('bypass_code')->defaultValue(null)->end() + ->arrayNode('valid_keys')->defaultValue(array('captcha'))->prototype('scalar')->end() ->end() ; + return $treeBuilder; } } diff --git a/DependencyInjection/GregwarCaptchaExtension.php b/DependencyInjection/GregwarCaptchaExtension.php index f1c7209..3734cb4 100755 --- a/DependencyInjection/GregwarCaptchaExtension.php +++ b/DependencyInjection/GregwarCaptchaExtension.php @@ -31,7 +31,6 @@ class GregwarCaptchaExtension extends Extension $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)); diff --git a/Generator/CaptchaGenerator.php b/Generator/CaptchaGenerator.php index 2620c94..0fbb878 100644 --- a/Generator/CaptchaGenerator.php +++ b/Generator/CaptchaGenerator.php @@ -4,6 +4,7 @@ namespace Gregwar\CaptchaBundle\Generator; use Symfony\Component\Finder\Finder; use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\Routing\RouterInterface; /** * Generates a CAPTCHA image @@ -15,6 +16,11 @@ class CaptchaGenerator */ protected $session; + /** + * @var \Symfony\Component\Routing\RouterInterface + */ + protected $router; + /** * Name of folder for captcha images * @var string @@ -51,28 +57,22 @@ class CaptchaGenerator */ protected $useFingerprint; - /** - * The key used to store the value to the session - * @var string - */ - protected $key = 'captcha'; - /** * @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session + * @param \Symfony\Component\Routing\RouterInterface $router * @param string $imageFolder * @param string $webPath * @param int $gcFreq * @param int $expiration - * @param string $url */ - public function __construct(SessionInterface $session, $imageFolder, $webPath, $gcFreq, $expiration, $url) + public function __construct(SessionInterface $session, RouterInterface $router, $imageFolder, $webPath, $gcFreq, $expiration) { $this->session = $session; + $this->router = $router; $this->imageFolder = $imageFolder; $this->webPath = $webPath; $this->gcFreq = $gcFreq; $this->expiration = $expiration; - $this->url = $url; } /** @@ -85,42 +85,40 @@ class CaptchaGenerator */ 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($options); + return $this->generate($key, $options); } // Returns the configured URL for image generation if ($options['as_url']) { - return $this->url; + return $this->router->generate('gregwar_captcha.generate_captcha', array('key' => $key)); } - return 'data:image/jpeg;base64,' . base64_encode($this->generate($options)); + return 'data:image/jpeg;base64,' . base64_encode($this->generate($key, $options)); } /** * Generate the image */ - public function generate(array $options) + public function generate($key, array $options) { $width = $options['width']; $height = $options['height']; - if ($options['keep_value'] && $this->session->has($this->key.'_fingerprint')) { - $this->fingerprint = $this->session->get($this->key.'_fingerprint'); + 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($options['keep_value'], $options['charset'], $options['length']); + $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)); @@ -185,7 +183,7 @@ class CaptchaGenerator } if ($options['keep_value']) { - $this->session->set($this->key.'_fingerprint', $this->fingerprint); + $this->session->set($key . '_fingerprint', $this->fingerprint); } // Renders it @@ -211,16 +209,17 @@ class CaptchaGenerator /** * 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($keepValue, $charset, $length) + protected function getCaptchaValue($key, $keepValue, $charset, $length) { - if ($keepValue && $this->session->has($this->key)) { - return $this->session->get($this->key); + if ($keepValue && $this->session->has($key)) { + return $this->session->get($key); } $value = ''; @@ -230,7 +229,7 @@ class CaptchaGenerator $value .= $chars[array_rand($chars)]; } - $this->session->set($this->key, $value); + $this->session->set($key, $value); return $value; } diff --git a/Resources/config/routing/routing.yml b/Resources/config/routing/routing.yml index 36126a3..e074029 100644 --- a/Resources/config/routing/routing.yml +++ b/Resources/config/routing/routing.yml @@ -1,3 +1,3 @@ gregwar_captcha.generate_captcha: - pattern: %gregwar_captcha.config.url% - defaults: { _controller: GregwarCaptchaBundle:Captcha:generateCaptcha } \ No newline at end of file + pattern: /generate-captcha/{key} + defaults: { _controller: GregwarCaptchaBundle:Captcha:generateCaptcha } \ No newline at end of file diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 3e13e64..dfb9fc0 100755 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -14,8 +14,8 @@ services: 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% - - %gregwar_captcha.config.url% + - %gregwar_captcha.config.expiration% \ No newline at end of file From fef3e306fd63b2a40b84950cfad9ad2091e8e12e Mon Sep 17 00:00:00 2001 From: Jeremy Livingston Date: Mon, 3 Dec 2012 14:55:11 -0500 Subject: [PATCH 3/6] Optimize formatting for Symfony coding standards --- Generator/CaptchaGenerator.php | 63 +++++++++++++++++----------------- Validator/CaptchaValidator.php | 3 +- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/Generator/CaptchaGenerator.php b/Generator/CaptchaGenerator.php index 0fbb878..d009b17 100644 --- a/Generator/CaptchaGenerator.php +++ b/Generator/CaptchaGenerator.php @@ -136,46 +136,45 @@ class CaptchaGenerator } // Write CAPTCHA text - $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]; + $size = $width / strlen($captchaValue); + $font = $options['font']; + $box = imagettfbbox($size, 0, $font, $captchaValue); + $textWidth = $box[2] - $box[0]; + $textHeight = $box[1] - $box[7]; - imagettftext($i, $size, 0, ($width-$txt_width)/2, ($height-$txt_height)/2+$size, $col, $font, $captchaValue); + imagettftext($i, $size, 0, ($width - $textWidth) / 2, ($height - $textHeight) / 2 + $size, $col, $font, $captchaValue); // Distort the image - $X = $this->rand(0, $width); - $Y = $this->rand(0, $height); - $Phase=$this->rand(0,10); - $Scale = 1.3 + $this->rand(0,10000)/30000; - $Amp=1+$this->rand(0,1000)/1000; - $out = imagecreatetruecolor($width, $height); + $X = $this->rand(0, $width); + $Y = $this->rand(0, $height); + $phase = $this->rand(0, 10); + $scale = 1.3 + $this->rand(0, 10000) / 30000; + $out = imagecreatetruecolor($width, $height); - for ($x=0; $x<$width; $x++) { - for ($y=0; $y<$height; $y++) { - $Vx=$x-$X; - $Vy=$y-$Y; - $Vn=sqrt($Vx*$Vx+$Vy*$Vy); + for ($x = 0; $x < $width; $x++) { + for ($y = 0; $y < $height; $y++) { + $Vx = $x - $X; + $Vy = $y - $Y; + $Vn = sqrt($Vx * $Vx + $Vy * $Vy); - if ($Vn!=0) { - $Vn2=$Vn+4*sin($Vn/8); - $nX=$X+($Vx*$Vn2/$Vn); - $nY=$Y+($Vy*$Vn2/$Vn); + if ($Vn != 0) { + $Vn2 = $Vn + 4 * sin($Vn / 8); + $nX = $X + ($Vx * $Vn2 / $Vn); + $nY = $Y + ($Vy * $Vn2 / $Vn); } else { - $nX=$X; - $nY=$Y; + $nX = $X; + $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), - $this->getCol($i,floor($nX),floor($nY)), - $this->getCol($i,ceil($nX),floor($nY)), - $this->getCol($i,floor($nX),ceil($nY)), - $this->getCol($i,ceil($nX),ceil($nY))); + $p = $this->bilinearInterpolate($nX - floor($nX), $nY - floor($nY), + $this->getCol($i, floor($nX), floor($nY)), + $this->getCol($i, ceil($nX), floor($nY)), + $this->getCol($i, floor($nX), ceil($nY)), + $this->getCol($i, ceil($nX), ceil($nY))); - if ($p==0) { - $p=0xFFFFFF; + if ($p == 0) { + $p = 0xFFFFFF; } imagesetpixel($out, $x, $y, $p); @@ -302,7 +301,7 @@ class CaptchaGenerator { $L = imagesx($image); $H = imagesy($image); - if ($x<0 || $x>=$L || $y<0 || $y>=$H) { + if ($x < 0 || $x >= $L || $y < 0 || $y >= $H) { return 0xFFFFFF; } diff --git a/Validator/CaptchaValidator.php b/Validator/CaptchaValidator.php index 166b08b..dedaf33 100644 --- a/Validator/CaptchaValidator.php +++ b/Validator/CaptchaValidator.php @@ -57,8 +57,7 @@ class CaptchaValidator $code = $form->getData(); $expectedCode = $this->getExpectedCode(); - if (!($code && is_string($code) - && ($this->compare($code, $expectedCode) || $this->compare($code, $this->bypassCode)))) { + if (!($code && is_string($code) && ($this->compare($code, $expectedCode) || $this->compare($code, $this->bypassCode)))) { $form->addError(new FormError($this->invalidMessage)); } From 4d7534351d01986d68cec78215bc0c93993cfd05 Mon Sep 17 00:00:00 2001 From: Jeremy Livingston Date: Mon, 3 Dec 2012 14:57:38 -0500 Subject: [PATCH 4/6] Update documentation --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index acc6956..fce8f30 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,6 @@ You can define the following configuration options globally: * **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: @@ -161,6 +160,12 @@ To use a URL to generate a captcha image, you must add the bundle's routing conf 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 + Form Theming ============ From ba9e0818a564813d59f6170bd0e0e8bd8af3b5fc Mon Sep 17 00:00:00 2001 From: Jeremy Livingston Date: Mon, 3 Dec 2012 15:01:46 -0500 Subject: [PATCH 5/6] Add "valid_keys" documentation --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index fce8f30..01059bf 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,7 @@ You can define the following configuration options globally or on the CaptchaTyp * **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) +* **valid_keys**: names that are able to be used for a captcha form type (default=[captcha]) Example : @@ -166,6 +167,11 @@ This will use the bundle's route of "/generate-captcha/{key}" to handle the gene 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 ============ From 1437f0c7e027373f366c75b69ad80b08315b5e8e Mon Sep 17 00:00:00 2001 From: Jeremy Livingston Date: Mon, 3 Dec 2012 18:54:39 -0500 Subject: [PATCH 6/6] Remove caching of form name for key --- Type/CaptchaType.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Type/CaptchaType.php b/Type/CaptchaType.php index ba2463b..9c91951 100644 --- a/Type/CaptchaType.php +++ b/Type/CaptchaType.php @@ -36,12 +36,6 @@ class CaptchaType extends AbstractType */ private $options = array(); - /** - * Session key - * @var string - */ - private $key = 'captcha'; - /** * @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session * @param \Gregwar\CaptchaBundle\Generator\CaptchaGenerator $generator @@ -60,9 +54,13 @@ class CaptchaType extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { - $this->key = $builder->getForm()->getName(); + $validator = new CaptchaValidator( + $this->session, + $builder->getForm()->getName(), + $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')); } @@ -76,7 +74,7 @@ class CaptchaType extends AbstractType $view->vars = array_merge($view->vars, array( 'captcha_width' => $options['width'], 'captcha_height' => $options['height'], - 'captcha_code' => $this->generator->getCaptchaCode($this->key, $options), + 'captcha_code' => $this->generator->getCaptchaCode($form->getName(), $options), 'value' => '', )); }