From bf006f52027f846863bcb7e7dcd112f115774481 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 16 Oct 2014 15:08:42 +0200 Subject: [PATCH] [Validator] Fixed Regex::getHtmlPattern() to work with complex and negated patterns --- .../Component/Validator/Constraints/Regex.php | 61 ++++++------- .../Validator/Tests/Constraints/RegexTest.php | 87 +++++++++++++++++++ .../Tests/Constraints/RegexValidatorTest.php | 66 -------------- 3 files changed, 118 insertions(+), 96 deletions(-) create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/RegexTest.php diff --git a/src/Symfony/Component/Validator/Constraints/Regex.php b/src/Symfony/Component/Validator/Constraints/Regex.php index aa4babba68..1f67246e3e 100644 --- a/src/Symfony/Component/Validator/Constraints/Regex.php +++ b/src/Symfony/Component/Validator/Constraints/Regex.php @@ -45,7 +45,14 @@ class Regex extends Constraint } /** - * Returns htmlPattern if exists or pattern is convertible. + * Converts the htmlPattern to a suitable format for HTML5 pattern. + * Example: /^[a-z]+$/ would be converted to [a-z]+ + * However, if options are specified, it cannot be converted + * + * Pattern is also ignored if match=false since the pattern should + * then be reversed before application. + * + * @link http://dev.w3.org/html5/spec/single-page.html#the-pattern-attribute * * @return string|null */ @@ -58,40 +65,34 @@ class Regex extends Constraint : $this->htmlPattern; } - return $this->getNonDelimitedPattern(); - } - - /** - * Converts the htmlPattern to a suitable format for HTML5 pattern. - * Example: /^[a-z]+$/ would be converted to [a-z]+ - * However, if options are specified, it cannot be converted - * - * Pattern is also ignored if match=false since the pattern should - * then be reversed before application. - * - * @todo reverse pattern in case match=false as per issue #5307 - * - * @link http://dev.w3.org/html5/spec/single-page.html#the-pattern-attribute - * - * @return string|null - */ - private function getNonDelimitedPattern() - { - // If match = false, pattern should not be added to HTML5 validation - if (!$this->match) { + // Quit if delimiters not at very beginning/end (e.g. when options are passed) + if ($this->pattern[0] !== $this->pattern[strlen($this->pattern) - 1]) { return; } - if (preg_match('/^(.)(\^?)(.*?)(\$?)\1$/', $this->pattern, $matches)) { - $delimiter = $matches[1]; - $start = empty($matches[2]) ? '.*' : ''; - $pattern = $matches[3]; - $end = empty($matches[4]) ? '.*' : ''; + $delimiter = $this->pattern[0]; - // Unescape the delimiter in pattern - $pattern = str_replace('\\'.$delimiter, $delimiter, $pattern); + // Unescape the delimiter + $pattern = str_replace('\\'.$delimiter, $delimiter, substr($this->pattern, 1, -1)); - return $start.$pattern.$end; + // If the pattern is inverted, we can simply wrap it in + // ((?!pattern).)* + if (!$this->match) { + return '((?!'.$pattern.').)*'; } + + // If the pattern contains an or statement, wrap the pattern in + // .*(pattern).* and quit. Otherwise we'd need to parse the pattern + if (false !== strpos($pattern, '|')) { + return '.*('.$pattern.').*'; + } + + // Trim leading ^, otherwise prepend .* + $pattern = '^' === $pattern[0] ? substr($pattern, 1) : '.*'.$pattern; + + // Trim trailing $, otherwise append .* + $pattern = '$' === $pattern[strlen($pattern) - 1] ? substr($pattern, 0, -1) : $pattern.'.*'; + + return $pattern; } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/RegexTest.php b/src/Symfony/Component/Validator/Tests/Constraints/RegexTest.php new file mode 100644 index 0000000000..ea37bb4d6b --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/RegexTest.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Constraints; + +use Symfony\Component\Validator\Constraints\Regex; + +/** + * @author Bernhard Schussek + */ +class RegexTest extends \PHPUnit_Framework_TestCase +{ + public function testConstraintGetDefaultOption() + { + $constraint = new Regex('/^[0-9]+$/'); + + $this->assertSame('/^[0-9]+$/', $constraint->pattern); + } + + public function provideHtmlPatterns() + { + return array( + // HTML5 wraps the pattern in ^(?:pattern)$ + array('/^[0-9]+$/', '[0-9]+'), + array('/[0-9]+$/', '.*[0-9]+'), + array('/^[0-9]+/', '[0-9]+.*'), + array('/[0-9]+/', '.*[0-9]+.*'), + // We need a smart way to allow matching of patterns that contain + // ^ and $ at various sub-clauses of an or-clause + // .*(pattern).* seems to work correctly + array('/[0-9]$|[a-z]+/', '.*([0-9]$|[a-z]+).*'), + array('/[0-9]$|^[a-z]+/', '.*([0-9]$|^[a-z]+).*'), + array('/^[0-9]|[a-z]+$/', '.*(^[0-9]|[a-z]+$).*'), + // Unescape escaped delimiters + array('/^[0-9]+\/$/', '[0-9]+/'), + array('#^[0-9]+\#$#', '[0-9]+#'), + // Cannot be converted + array('/^[0-9]+$/i', null), + + // Inverse matches are simple, just wrap in + // ((?!pattern).)* + array('/^[0-9]+$/', '((?!^[0-9]+$).)*', false), + array('/[0-9]+$/', '((?![0-9]+$).)*', false), + array('/^[0-9]+/', '((?!^[0-9]+).)*', false), + array('/[0-9]+/', '((?![0-9]+).)*', false), + array('/[0-9]$|[a-z]+/', '((?![0-9]$|[a-z]+).)*', false), + array('/[0-9]$|^[a-z]+/', '((?![0-9]$|^[a-z]+).)*', false), + array('/^[0-9]|[a-z]+$/', '((?!^[0-9]|[a-z]+$).)*', false), + array('/^[0-9]+\/$/', '((?!^[0-9]+/$).)*', false), + array('#^[0-9]+\#$#', '((?!^[0-9]+#$).)*', false), + array('/^[0-9]+$/i', null, false), + ); + } + + /** + * @dataProvider provideHtmlPatterns + */ + public function testGetHtmlPattern($pattern, $htmlPattern, $match = true) + { + $constraint = new Regex(array( + 'pattern' => $pattern, + 'match' => $match, + )); + + $this->assertSame($pattern, $constraint->pattern); + $this->assertSame($htmlPattern, $constraint->getHtmlPattern()); + } + + public function testGetCustomHtmlPattern() + { + $constraint = new Regex(array( + 'pattern' => '((?![0-9]$|[a-z]+).)*', + 'htmlPattern' => 'foobar', + )); + + $this->assertSame('((?![0-9]$|[a-z]+).)*', $constraint->pattern); + $this->assertSame('foobar', $constraint->getHtmlPattern()); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/RegexValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/RegexValidatorTest.php index 8afb037752..2bbcaa16de 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/RegexValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/RegexValidatorTest.php @@ -88,70 +88,4 @@ class RegexValidatorTest extends AbstractConstraintValidatorTest array('090foo'), ); } - - public function testConstraintGetDefaultOption() - { - $constraint = new Regex(array( - 'pattern' => '/^[0-9]+$/', - )); - - $this->assertEquals('pattern', $constraint->getDefaultOption()); - } - - public function testHtmlPatternEscaping() - { - $constraint = new Regex(array( - 'pattern' => '/^[0-9]+\/$/', - )); - - $this->assertEquals('[0-9]+/', $constraint->getHtmlPattern()); - - $constraint = new Regex(array( - 'pattern' => '#^[0-9]+\#$#', - )); - - $this->assertEquals('[0-9]+#', $constraint->getHtmlPattern()); - } - - public function testHtmlPattern() - { - // Specified htmlPattern - $constraint = new Regex(array( - 'pattern' => '/^[a-z]+$/i', - 'htmlPattern' => '[a-zA-Z]+', - )); - $this->assertEquals('[a-zA-Z]+', $constraint->getHtmlPattern()); - - // Disabled htmlPattern - $constraint = new Regex(array( - 'pattern' => '/^[a-z]+$/i', - 'htmlPattern' => false, - )); - $this->assertNull($constraint->getHtmlPattern()); - - // Cannot be converted - $constraint = new Regex(array( - 'pattern' => '/^[a-z]+$/i', - )); - $this->assertNull($constraint->getHtmlPattern()); - - // Automatically converted - $constraint = new Regex(array( - 'pattern' => '/^[a-z]+$/', - )); - $this->assertEquals('[a-z]+', $constraint->getHtmlPattern()); - - // Automatically converted, adds .* - $constraint = new Regex(array( - 'pattern' => '/[a-z]+/', - )); - $this->assertEquals('.*[a-z]+.*', $constraint->getHtmlPattern()); - - // Dropped because of match=false - $constraint = new Regex(array( - 'pattern' => '/[a-z]+/', - 'match' => false, - )); - $this->assertNull($constraint->getHtmlPattern()); - } }