From ce1b590ae924bbc1b9a1f6da34c413aa557c7228 Mon Sep 17 00:00:00 2001 From: Jeremy Livingston Date: Mon, 3 Dec 2012 18:48:32 -0500 Subject: [PATCH 1/2] Separate generation concerns into different services --- Generator/CaptchaGenerator.php | 262 ++++++--------------------------- Generator/ImageBuilder.php | 192 ++++++++++++++++++++++++ Generator/ImageFileHandler.php | 103 +++++++++++++ Resources/config/services.yml | 13 +- Type/CaptchaType.php | 16 +- 5 files changed, 356 insertions(+), 230 deletions(-) create mode 100644 Generator/ImageBuilder.php create mode 100644 Generator/ImageFileHandler.php diff --git a/Generator/CaptchaGenerator.php b/Generator/CaptchaGenerator.php index d009b17..7c2e099 100644 --- a/Generator/CaptchaGenerator.php +++ b/Generator/CaptchaGenerator.php @@ -22,57 +22,27 @@ class CaptchaGenerator protected $router; /** - * Name of folder for captcha images - * @var string + * @var ImageBuilder */ - protected $imageFolder; + protected $builder; /** - * Absolute path to public web folder - * @var string + * @var ImageFileHandler */ - protected $webPath; - - /** - * Frequency of garbage collection in fractions of 1 - * @var int - */ - protected $gcFreq; - - /** - * Maximum age of images in minutes - * @var int - */ - protected $expiration; - - /** - * The fingerprint used to generate the image details across requests - * @var array|null - */ - protected $fingerprint; - - /** - * Whether this instance should use the fingerprint - * @var bool - */ - protected $useFingerprint; + protected $imageFileHandler; /** * @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 ImageBuilder $builder + * @param ImageFileHandler $imageFileHandler */ - public function __construct(SessionInterface $session, RouterInterface $router, $imageFolder, $webPath, $gcFreq, $expiration) + public function __construct(SessionInterface $session, RouterInterface $router, ImageBuilder $builder, ImageFileHandler $imageFileHandler) { $this->session = $session; $this->router = $router; - $this->imageFolder = $imageFolder; - $this->webPath = $webPath; - $this->gcFreq = $gcFreq; - $this->expiration = $expiration; + $this->builder = $builder; + $this->imageFileHandler = $imageFileHandler; } /** @@ -87,14 +57,12 @@ class CaptchaGenerator { // Randomly execute garbage collection and returns the image filename if ($options['as_file']) { - if (mt_rand(1, $this->gcFreq) == 1) { - $this->garbageCollection(); - } + $this->imageFileHandler->collectGarbage(); return $this->generate($key, $options); } - // Returns the configured URL for image generation + // Returns the image generation URL if ($options['as_url']) { return $this->router->generate('gregwar_captcha.generate_captcha', array('key' => $key)); } @@ -103,217 +71,75 @@ class CaptchaGenerator } /** - * Generate the image + * @param string $key + * @param array $options + * + * @return string */ public function generate($key, array $options) { - $width = $options['width']; - $height = $options['height']; + $fingerprint = $this->getFingerprint($key, $options); - 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)); - - imagefill($i, 0, 0, 0xFFFFFF); - - // Draw random lines - for ($t=0; $t<10; $t++) { - $tcol = imagecolorallocate($i, 100+$this->rand(0,150), 100+$this->rand(0,150), 100+$this->rand(0,150)); - $Xa = $this->rand(0, $width); - $Ya = $this->rand(0, $height); - $Xb = $this->rand(0, $width); - $Yb = $this->rand(0, $height); - imageline($i, $Xa, $Ya, $Xb, $Yb, $tcol); - } - - // Write CAPTCHA text - $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 - $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; - $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); - - if ($Vn != 0) { - $Vn2 = $Vn + 4 * sin($Vn / 8); - $nX = $X + ($Vx * $Vn2 / $Vn); - $nY = $Y + ($Vy * $Vn2 / $Vn); - } else { - $nX = $X; - $nY = $Y; - } - $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))); - - if ($p == 0) { - $p = 0xFFFFFF; - } - - imagesetpixel($out, $x, $y, $p); - } - } + $content = $this->builder->build( + $options['width'], + $options['height'], + $options['font'], + $this->getPhrase($key, $options), + $fingerprint + ); if ($options['keep_value']) { - $this->session->set($key . '_fingerprint', $this->fingerprint); + $this->session->set($key . '_fingerprint', $this->builder->getFingerprint()); } - // Renders it if (!$options['as_file']) { ob_start(); - imagejpeg($out, null, $options['quality']); + imagejpeg($content, null, $options['quality']); return ob_get_clean(); } - // 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; + return $this->imageFileHandler->saveAsFile($content); } /** - * 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 + * @param array $options * - * @return mixed|string + * @return string */ - protected function getCaptchaValue($key, $keepValue, $charset, $length) + protected function getPhrase($key, array $options) { - if ($keepValue && $this->session->has($key)) { + // Get the phrase that we'll use for this image + if ($options['keep_value'] && $this->session->has($key)) { return $this->session->get($key); } - $value = ''; - $chars = str_split($charset); + $phrase = ''; + $chars = str_split($options['charset']); - for ($i=0; $i < $length; $i++) { - $value .= $chars[array_rand($chars)]; + for ($i = 0; $i < $options['length']; $i++) { + $phrase .= $chars[array_rand($chars)]; } - $this->session->set($key, $value); + $this->session->set($key, $phrase); - return $value; + return $phrase; } /** - * Deletes all images in the configured folder - * that are older than the configured number of minutes + * @param string $key + * @param array $options * - * @return void + * @return array|null */ - protected function garbageCollection() + protected function getFingerprint($key, array $options) { - $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 ($options['keep_value'] && $this->session->has($key . '_fingerprint')) { + return $this->session->get($key . '_fingerprint'); } - 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); - list($r2, $g2, $b2) = $this->getRGB($sw); - list($r3, $g3, $b3) = $this->getRGB($se); - - $cx = 1.0 - $x; - $cy = 1.0 - $y; - - $m0 = $cx * $r0 + $x * $r1; - $m1 = $cx * $r2 + $x * $r3; - $r = (int)($cy * $m0 + $y * $m1); - - $m0 = $cx * $g0 + $x * $g1; - $m1 = $cx * $g2 + $x * $g3; - $g = (int)($cy * $m0 + $y * $m1); - - $m0 = $cx * $b0 + $x * $b1; - $m1 = $cx * $b2 + $x * $b3; - $b = (int)($cy * $m0 + $y * $m1); - - 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, - ); + return null; } } diff --git a/Generator/ImageBuilder.php b/Generator/ImageBuilder.php new file mode 100644 index 0000000..9f1e68b --- /dev/null +++ b/Generator/ImageBuilder.php @@ -0,0 +1,192 @@ +fingerprint = $fingerprint; + $this->useFingerprint = true; + } else { + $this->fingerprint = array(); + $this->useFingerprint = false; + } + + $i = imagecreatetruecolor($width, $height); + $col = imagecolorallocate($i, $this->rand(0, 110), $this->rand(0, 110), $this->rand(0, 110)); + + imagefill($i, 0, 0, 0xFFFFFF); + + // Draw random lines + for ($t = 0; $t < 10; $t++) { + $tcol = imagecolorallocate($i, 100 + $this->rand(0, 150), 100 + $this->rand(0, 150), 100 + $this->rand(0, 150)); + $Xa = $this->rand(0, $width); + $Ya = $this->rand(0, $height); + $Xb = $this->rand(0, $width); + $Yb = $this->rand(0, $height); + imageline($i, $Xa, $Ya, $Xb, $Yb, $tcol); + } + + // Write CAPTCHA text + $size = $width / strlen($phrase); + $box = imagettfbbox($size, 0, $font, $phrase); + $textWidth = $box[2] - $box[0]; + $textHeight = $box[1] - $box[7]; + + imagettftext($i, $size, 0, ($width - $textWidth) / 2, ($height - $textHeight) / 2 + $size, $col, $font, $phrase); + + // 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; + $contents = 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); + + if ($Vn != 0) { + $Vn2 = $Vn + 4 * sin($Vn / 8); + $nX = $X + ($Vx * $Vn2 / $Vn); + $nY = $Y + ($Vy * $Vn2 / $Vn); + } else { + $nX = $X; + $nY = $Y; + } + $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))); + + if ($p == 0) { + $p = 0xFFFFFF; + } + + imagesetpixel($contents, $x, $y, $p); + } + } + + return $contents; + } + + /** + * @return array + */ + public function getFingerprint() + { + return $this->fingerprint; + } + + /** + * 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; + } + + /** + * @param $x + * @param $y + * @param $nw + * @param $ne + * @param $sw + * @param $se + * + * @return int + */ + protected function bilinearInterpolate($x, $y, $nw, $ne, $sw, $se) + { + list($r0, $g0, $b0) = $this->getRGB($nw); + list($r1, $g1, $b1) = $this->getRGB($ne); + list($r2, $g2, $b2) = $this->getRGB($sw); + list($r3, $g3, $b3) = $this->getRGB($se); + + $cx = 1.0 - $x; + $cy = 1.0 - $y; + + $m0 = $cx * $r0 + $x * $r1; + $m1 = $cx * $r2 + $x * $r3; + $r = (int)($cy * $m0 + $y * $m1); + + $m0 = $cx * $g0 + $x * $g1; + $m1 = $cx * $g2 + $x * $g3; + $g = (int)($cy * $m0 + $y * $m1); + + $m0 = $cx * $b0 + $x * $b1; + $m1 = $cx * $b2 + $x * $b3; + $b = (int)($cy * $m0 + $y * $m1); + + return ($r << 16) | ($g << 8) | $b; + } + + /** + * @param $image + * @param $x + * @param $y + * + * @return int + */ + 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); + } + + /** + * @param $col + * + * @return array + */ + protected function getRGB($col) + { + return array( + (int)($col >> 16) & 0xff, + (int)($col >> 8) & 0xff, + (int)($col) & 0xff, + ); + } +} + diff --git a/Generator/ImageFileHandler.php b/Generator/ImageFileHandler.php new file mode 100644 index 0000000..2611401 --- /dev/null +++ b/Generator/ImageFileHandler.php @@ -0,0 +1,103 @@ + + */ +class ImageFileHandler +{ + /** + * Name of folder for captcha images + * @var string + */ + protected $imageFolder; + + /** + * Absolute path to public web folder + * @var string + */ + protected $webPath; + + /** + * Frequency of garbage collection in fractions of 1 + * @var int + */ + protected $gcFreq; + + /** + * Maximum age of images in minutes + * @var int + */ + protected $expiration; + + /** + * @param $imageFolder + * @param $webPath + * @param $gcFreq + * @param $expiration + */ + public function __construct($imageFolder, $webPath, $gcFreq, $expiration) + { + $this->imageFolder = $imageFolder; + $this->webPath = $webPath; + $this->gcFreq = $gcFreq; + $this->expiration = $expiration; + } + + /** + * Saves the provided image content as a file + * + * @param string $contents + * + * @return string + */ + public function saveAsFile($contents) + { + $this->createFolderIfMissing(); + + $filename = md5(uniqid()) . '.jpg'; + $filePath = $this->webPath . '/' . $this->imageFolder . '/' . $filename; + imagejpeg($contents, $filePath, 15); + + return '/' . $this->imageFolder . '/' . $filename; + } + + /** + * Randomly runs garbage collection on the image directory + * + * @return bool + */ + public function collectGarbage() + { + if (!mt_rand(1, $this->gcFreq) == 1) { + return false; + } + + $this->createFolderIfMissing(); + + $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 true; + } + + /** + * Creates the folder if it doesn't exist + */ + protected function createFolderIfMissing() + { + if (!file_exists($this->webPath . '/' . $this->imageFolder)) { + mkdir($this->webPath . '/' . $this->imageFolder, 0755); + } + } +} + diff --git a/Resources/config/services.yml b/Resources/config/services.yml index dfb9fc0..8efec64 100755 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -1,6 +1,4 @@ - services: - # captcha type captcha.type: class: Gregwar\CaptchaBundle\Type\CaptchaType arguments: @@ -15,7 +13,16 @@ services: arguments: - @session - @router + - @gregwar_captcha.image_builder + - @gregwar_captcha.image_file_handler + + gregwar_captcha.image_file_handler: + class: Gregwar\CaptchaBundle\Generator\ImageFileHandler + arguments: - %gregwar_captcha.config.image_folder% - %gregwar_captcha.config.web_path% - %gregwar_captcha.config.gc_freq% - - %gregwar_captcha.config.expiration% \ No newline at end of file + - %gregwar_captcha.config.expiration% + + gregwar_captcha.image_builder: + class: Gregwar\CaptchaBundle\Generator\ImageBuilder \ No newline at end of file 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' => '', )); } From 8885e6bcacdab7ec406501f2290a70ebb63e9b71 Mon Sep 17 00:00:00 2001 From: Jeremy Livingston Date: Mon, 3 Dec 2012 19:02:43 -0500 Subject: [PATCH 2/2] Add class documentation --- Generator/CaptchaGenerator.php | 5 ++++- Generator/ImageBuilder.php | 8 +++++++- Generator/ImageFileHandler.php | 3 +++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Generator/CaptchaGenerator.php b/Generator/CaptchaGenerator.php index 7c2e099..6af0b91 100644 --- a/Generator/CaptchaGenerator.php +++ b/Generator/CaptchaGenerator.php @@ -7,7 +7,10 @@ use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\Routing\RouterInterface; /** - * Generates a CAPTCHA image + * Uses configuration parameters to call the services that generate captcha images + * + * @author Gregwar + * @author Jeremy Livingston */ class CaptchaGenerator { diff --git a/Generator/ImageBuilder.php b/Generator/ImageBuilder.php index 9f1e68b..f94006d 100644 --- a/Generator/ImageBuilder.php +++ b/Generator/ImageBuilder.php @@ -4,7 +4,13 @@ namespace Gregwar\CaptchaBundle\Generator; use Symfony\Component\Finder\Finder; - +/** + * Builds a new captcha image + * Uses the fingerprint parameter, if one is passed, to generate the same image + * + * @author Gregwar + * @author Jeremy Livingston + */ class ImageBuilder { /** diff --git a/Generator/ImageFileHandler.php b/Generator/ImageFileHandler.php index 2611401..1a47420 100644 --- a/Generator/ImageFileHandler.php +++ b/Generator/ImageFileHandler.php @@ -5,6 +5,9 @@ namespace Gregwar\CaptchaBundle\Generator; use Symfony\Component\Finder\Finder; /** + * Handles actions related to captcha image files including saving and garbage collection + * + * @author Gregwar * @author Jeremy Livingston */ class ImageFileHandler