From fdf01d9d2c701e7cce4008d986012faf461689bb Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 27 Jun 2016 17:57:40 +0200 Subject: [PATCH] Handle PHP expressions properly #5 (#8) * Expressions in attributes * Fix parentheses ending * Fix attributes escaping --- src/Jade/Compiler.php | 2 +- src/Jade/Compiler/AttributesCompiler.php | 12 ++++----- src/Jade/Compiler/CodeHandler.php | 22 +++------------- src/Jade/Compiler/CodeVisitor.php | 3 +-- src/Jade/Compiler/CompilerConfig.php | 4 +-- src/Jade/Compiler/CompilerFacade.php | 22 ++++++++++++++-- src/Jade/Compiler/CompilerUtils.php | 25 +++++++++++++++---- tests/features/cache.php | 3 +++ tests/features/settings.php | 22 ++++++++++++++++ tests/lib/bootstrap.php | 2 ++ tests/templates/attrs-data.complex.html | 8 +++--- .../attrs-data.complex.single-quote.html | 5 ++++ tests/templates/attrs-data.html | 4 +-- tests/templates/attrs-data.single-quote.html | 5 ++++ tests/templates/attrs.expressions.jade | 4 +-- .../mixin.attrs-keep-null-attributes.html | 14 +++++------ 16 files changed, 106 insertions(+), 51 deletions(-) create mode 100644 tests/templates/attrs-data.complex.single-quote.html create mode 100644 tests/templates/attrs-data.single-quote.html diff --git a/src/Jade/Compiler.php b/src/Jade/Compiler.php index adca62dc..25ecc664 100644 --- a/src/Jade/Compiler.php +++ b/src/Jade/Compiler.php @@ -287,7 +287,7 @@ public function interpolate($text) protected function interpolateFromCapture($match) { if ($match[1] === '') { - return $this->createCode($match[2] === '!' ? static::UNESCAPED : static::ESCAPED, $match[3]); + return $this->escapeIfNeeded($match[2] === '!', $match[3]); } return substr($match[0], 1); diff --git a/src/Jade/Compiler/AttributesCompiler.php b/src/Jade/Compiler/AttributesCompiler.php index 7e7f74b0..7f96f479 100644 --- a/src/Jade/Compiler/AttributesCompiler.php +++ b/src/Jade/Compiler/AttributesCompiler.php @@ -69,18 +69,18 @@ protected function getClassAttribute($value, &$classesCheck) return $this->keepNullAttributes ? '' : 'null'; } - protected function getUnescapedValueCode($value, &$valueCheck) + protected function getValueCode($escaped, $value, &$valueCheck) { if ($this->keepNullAttributes) { - return $this->createCode(static::UNESCAPED, $value); + return $this->escapeIfNeeded($escaped, $value); } $valueCheck = $value; - return $this->createCode(static::UNESCAPED, '$__value'); + return $this->escapeIfNeeded($escaped, '$__value'); } - protected function getAttributeValue($key, $value, &$classesCheck, &$valueCheck) + protected function getAttributeValue($escaped, $key, $value, &$classesCheck, &$valueCheck) { if ($this->isConstant($value) || ($key != 'class' && $this->isArrayOfConstants($value))) { $value = trim($value, ' \'"'); @@ -98,7 +98,7 @@ protected function getAttributeValue($key, $value, &$classesCheck, &$valueCheck) return $this->getClassAttribute($value, $classesCheck); } - return $this->getUnescapedValueCode($value, $valueCheck); + return $this->getValueCode($escaped, $value, $valueCheck); } protected function compileAttributeValue($key, $value, $attr, $valueCheck) @@ -125,7 +125,7 @@ protected function getAttributeCode($attr, &$classes, &$classesCheck) $valueCheck = null; $value = trim($attr['value']); - $value = $this->getAttributeValue($key, $value, $classesCheck, $valueCheck); + $value = $this->getAttributeValue($attr['escaped'], $key, $value, $classesCheck, $valueCheck); if ($key === 'class') { if ($value !== 'false' && $value !== 'null' && $value !== 'undefined') { diff --git a/src/Jade/Compiler/CodeHandler.php b/src/Jade/Compiler/CodeHandler.php index 8c8e9ab9..9964322a 100644 --- a/src/Jade/Compiler/CodeHandler.php +++ b/src/Jade/Compiler/CodeHandler.php @@ -39,7 +39,7 @@ public function parse() return array($this->input); } - if (strpos('=])},;?', substr($this->input, 0, 1)) !== false) { + if (strpos('=,;?', substr($this->input, 0, 1)) !== false) { throw new \Exception('Expecting a variable name or an expression, got: ' . $this->input); } @@ -94,13 +94,7 @@ protected function parseBetweenSeparators() $getMiddleString = function ($start, $end) use ($input) { $offset = $start[1] + strlen($start[0]); - return substr( - $input, - $offset, - isset($end) - ? $end[1] - $offset - : strlen($input) - ); + return substr($input, $offset, isset($end) ? $end[1] - $offset : strlen($input)); }; $host = $this; @@ -153,7 +147,7 @@ protected function parseBetweenSeparators() if ($curr[0] === $close) { $count--; } - } while ($curr[0] !== null && $count > 0 && $curr[0] !== ','); + } while ($curr[0] !== null && $count >= 0 && $curr[0] !== ','); $end = current($separators); @@ -165,7 +159,7 @@ protected function parseBetweenSeparators() } } while ($curr !== false && $count > 0); - if ($close && $count) { + if ($close && $count > 0) { throw new \Exception($input . "\nMissing closing: " . $close); } @@ -220,14 +214,6 @@ protected function parseBetweenSeparators() array_push($result, "{$var}={$call}"); break; - // mixin arguments - case ',': - $arguments = $handleCodeInbetween(); - if ($arguments) { - $varname .= ', ' . implode(', ', $arguments); - } - break; - case '[': if (preg_match('/[a-zA-Z0-9\\\\_\\x7f-\\xff]$/', $varname)) { $varname .= $sep[0] . $innerName; diff --git a/src/Jade/Compiler/CodeVisitor.php b/src/Jade/Compiler/CodeVisitor.php index 51d92384..1be12295 100644 --- a/src/Jade/Compiler/CodeVisitor.php +++ b/src/Jade/Compiler/CodeVisitor.php @@ -51,8 +51,7 @@ protected function visitCodeOpening(Code $node) $code = trim($node->value); if ($node->buffer) { - $pattern = $node->escape ? static::ESCAPED : static::UNESCAPED; - $this->buffer($this->createCode($pattern, $code)); + $this->buffer($this->escapeIfNeeded($node->escape, $code)); return; } diff --git a/src/Jade/Compiler/CompilerConfig.php b/src/Jade/Compiler/CompilerConfig.php index a3db5302..90ed3665 100644 --- a/src/Jade/Compiler/CompilerConfig.php +++ b/src/Jade/Compiler/CompilerConfig.php @@ -47,12 +47,12 @@ abstract class CompilerConfig /** * @const string */ - const ESCAPED = 'echo htmlspecialchars(%s)'; + const ESCAPED = 'echo \\Jade\\Compiler::getEscapedValue(%s, %s)'; /** * @const string */ - const UNESCAPED = 'echo \\Jade\\Compiler::strval(%s)'; + const UNESCAPED = 'echo \\Jade\\Compiler::getUnescapedValue(%s)'; /** * @var array diff --git a/src/Jade/Compiler/CompilerFacade.php b/src/Jade/Compiler/CompilerFacade.php index 290d6bbc..65338a1f 100644 --- a/src/Jade/Compiler/CompilerFacade.php +++ b/src/Jade/Compiler/CompilerFacade.php @@ -17,9 +17,27 @@ abstract class CompilerFacade extends CompilerUtils * * @return string */ - public static function strval($val) + public static function getUnescapedValue($val) { - return is_array($val) || is_null($val) || is_bool($val) || is_int($val) || is_float($val) ? json_encode($val) : strval($val); + if (is_null($val) || $val === false || $val === '') { + return ''; + } + + return is_array($val) || is_bool($val) || is_int($val) || is_float($val) ? json_encode($val) : strval($val); + } + + /** + * value treatment if it must not be escaped. + * + * @param string input value + * + * @return string + */ + public static function getEscapedValue($val, $quote) + { + $val = htmlspecialchars(static::getUnescapedValue($val), ENT_NOQUOTES); + + return str_replace($quote, $quote === '"' ? '"' : ''', $val); } /** diff --git a/src/Jade/Compiler/CompilerUtils.php b/src/Jade/Compiler/CompilerUtils.php index ac2d8508..45872c32 100644 --- a/src/Jade/Compiler/CompilerUtils.php +++ b/src/Jade/Compiler/CompilerUtils.php @@ -9,7 +9,7 @@ abstract class CompilerUtils extends Indenter { /** - * @param string $call + * @param $call string * * @throws \Exception * @@ -78,7 +78,7 @@ protected static function initArgToNull(&$arg) } /** - * @param string $value + * @param $value string * * @return mixed */ @@ -88,7 +88,7 @@ protected static function parseValue($value) } /** - * @param string $value + * @param $value string * * @return mixed */ @@ -100,7 +100,7 @@ protected static function decodeValue($value) } /** - * @param array $attributes + * @param $attributes array * * @return array */ @@ -119,7 +119,7 @@ protected static function decodeAttributes($attributes) } /** - * @param $name + * @param $name string * * @return callable */ @@ -129,4 +129,19 @@ protected function getFilter($name) return $helper->getValidFilter($name); } + + /** + * @param $escaped bool need to be escaped + * @param $value mixed to be escaped if $escaped is true + * + * @return callable + */ + protected function escapeIfNeeded($escaped, $value) + { + if ($escaped) { + return $this->createCode(static::ESCAPED, $value, var_export($this->quote, true)); + } + + return $this->createCode(static::UNESCAPED, $value); + } } diff --git a/tests/features/cache.php b/tests/features/cache.php index e48727d1..7197f264 100644 --- a/tests/features/cache.php +++ b/tests/features/cache.php @@ -40,6 +40,7 @@ protected function emptyDirectory($dir) public function testMissingDirectory() { $jade = new Jade(array( + 'singleQuote' => false, 'cache' => 'does/not/exists' )); $jade->render(__DIR__ . '/../templates/attrs.jade'); @@ -93,6 +94,7 @@ public function testReadOnlyDirectory() { $dir = $parent; } $jade = new Jade(array( + 'singleQuote' => false, 'cache' => $dir )); $jade->cache(__DIR__ . '/../templates/attrs.jade'); @@ -102,6 +104,7 @@ private function cacheSystem($keepBaseName) { $file = tempnam(sys_get_temp_dir(), 'jade-test-'); $jade = new Jade(array( + 'singleQuote' => false, 'keepBaseName' => $keepBaseName, 'cache' => sys_get_temp_dir(), )); diff --git a/tests/features/settings.php b/tests/features/settings.php index 7ec53abb..2bdc8f09 100644 --- a/tests/features/settings.php +++ b/tests/features/settings.php @@ -13,12 +13,18 @@ static private function rawHtml($html, $convertSingleQuote = true) { return trim(preg_replace('`\n{2,}`', "\n", $html)); } + static private function simpleHtml($html) { + + return trim(preg_replace('`\r\n|\r`', "\n", $html)); + } + /** * keepNullAttributes setting test */ public function testKeepNullAttributes() { $jade = new Jade(array( + 'singleQuote' => false, 'keepNullAttributes' => false, 'prettyprint' => true, )); @@ -29,6 +35,7 @@ public function testKeepNullAttributes() { $this->assertSame(static::rawHtml($actual), static::rawHtml($expected), 'Keep null attributes disabled'); $jade = new Jade(array( + 'singleQuote' => false, 'keepNullAttributes' => true, 'prettyprint' => true, )); @@ -59,6 +66,7 @@ public function testPrettyprint() { '; $jade = new Jade(array( + 'singleQuote' => true, 'prettyprint' => true, )); $actual = trim(preg_replace('`\n[\s\n]+`', "\n", str_replace("\r", '', preg_replace('`[ \t]+`', ' ', $jade->render($template))))); @@ -70,6 +78,7 @@ public function testPrettyprint() { $this->assertSame($expected, $actual, 'Pretty print enabled'); $jade = new Jade(array( + 'singleQuote' => true, 'prettyprint' => false, )); $actual = preg_replace('`[ \t]+`', ' ', $jade->render($template)); @@ -98,6 +107,7 @@ public function testSetOption() { '; $jade = new Jade(array( + 'singleQuote' => true, 'prettyprint' => false, )); $this->assertFalse($jade->getOption('prettyprint'), 'getOption should return current setting'); @@ -175,6 +185,7 @@ public function testAllowMixinOverride() { '; $jade = new Jade(array( + 'singleQuote' => false, 'allowMixinOverride' => true, )); $actual = $jade->render($template); @@ -183,6 +194,7 @@ public function testAllowMixinOverride() { $this->assertSame(static::rawHtml($actual), static::rawHtml($expected), 'Allow mixin override enabled'); $jade = new Jade(array( + 'singleQuote' => false, 'allowMixinOverride' => false, )); $actual = $jade->render($template); @@ -236,20 +248,30 @@ public function testSingleQuote() { $template = 'h1#foo.bar(style="color: red;") Hello'; $jade = new Jade(array( + 'prettyprint' => true, 'singleQuote' => true, )); $actual = $jade->render($template); $expected = "

Hello

"; $this->assertSame(static::rawHtml($actual, false), static::rawHtml($expected, false), 'Single quote enabled'); + $file = __DIR__ . '/../templates/attrs-data.complex'; + $this->assertSame(static::simpleHtml($jade->render($file . '.jade')), static::simpleHtml(file_get_contents($file . '.single-quote.html')), 'Single quote enabled'); + $file = __DIR__ . '/../templates/attrs-data'; + $this->assertSame(static::simpleHtml($jade->render($file . '.jade')), static::simpleHtml(file_get_contents($file . '.single-quote.html')), 'Single quote enabled'); $jade = new Jade(array( + 'prettyprint' => true, 'singleQuote' => false, )); $actual = $jade->render($template); $expected = '

Hello

'; $this->assertSame(static::rawHtml($actual, false), static::rawHtml($expected, false), 'Single quote disabled'); + $file = __DIR__ . '/../templates/attrs-data.complex'; + $this->assertSame(static::simpleHtml($jade->render($file . '.jade')), static::simpleHtml(file_get_contents($file . '.html')), 'Single quote enabled'); + $file = __DIR__ . '/../templates/attrs-data'; + $this->assertSame(static::simpleHtml($jade->render($file . '.jade')), static::simpleHtml(file_get_contents($file . '.html')), 'Single quote enabled'); } /** diff --git a/tests/lib/bootstrap.php b/tests/lib/bootstrap.php index 8b9323c2..17072e15 100644 --- a/tests/lib/bootstrap.php +++ b/tests/lib/bootstrap.php @@ -44,6 +44,7 @@ function build_list($test_list) { function get_php_code($file) { $jade = new Jade(array( + 'singleQuote' => false, 'prettyprint' => true, )); return $jade->render($file); @@ -51,6 +52,7 @@ function get_php_code($file) { function compile_php($file) { $jade = new Jade(array( + 'singleQuote' => false, 'prettyprint' => true, )); return $jade->compile(file_get_contents(TEMPLATES_DIRECTORY . DIRECTORY_SEPARATOR . $file . '.jade')); diff --git a/tests/templates/attrs-data.complex.html b/tests/templates/attrs-data.complex.html index 381e4c69..c11e368a 100644 --- a/tests/templates/attrs-data.complex.html +++ b/tests/templates/attrs-data.complex.html @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/tests/templates/attrs-data.complex.single-quote.html b/tests/templates/attrs-data.complex.single-quote.html new file mode 100644 index 00000000..99e9a93d --- /dev/null +++ b/tests/templates/attrs-data.complex.single-quote.html @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/templates/attrs-data.html b/tests/templates/attrs-data.html index 4c146304..2112b34f 100644 --- a/tests/templates/attrs-data.html +++ b/tests/templates/attrs-data.html @@ -1,5 +1,5 @@ - - + + diff --git a/tests/templates/attrs-data.single-quote.html b/tests/templates/attrs-data.single-quote.html new file mode 100644 index 00000000..87a5e9d4 --- /dev/null +++ b/tests/templates/attrs-data.single-quote.html @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/templates/attrs.expressions.jade b/tests/templates/attrs.expressions.jade index ebf9f78c..f62cbe7d 100644 --- a/tests/templates/attrs.expressions.jade +++ b/tests/templates/attrs.expressions.jade @@ -8,5 +8,5 @@ $ob->foo['bar'] = new FooClass(); p(answear=ob->foo["bar"]->met()) p(answear=(ob->foo["bar"]->met())) -p(answear=substr("foobar", strlen("foo"))) -p(answear=substr("foobar", strlen(strtoupper("F") . "oo"))) +p(answear=substr("foo-bar", strlen("foo") + 1)) +p(answear=substr("foo-bar", strlen(strtoupper("F") . "oo") + 1)) diff --git a/tests/templates/mixin.attrs-keep-null-attributes.html b/tests/templates/mixin.attrs-keep-null-attributes.html index 1567b164..4e1051fb 100644 --- a/tests/templates/mixin.attrs-keep-null-attributes.html +++ b/tests/templates/mixin.attrs-keep-null-attributes.html @@ -12,7 +12,7 @@

Section 2

-
+

Section 3' and quote""

Last content.

@@ -25,13 +25,13 @@

Section 3' and quote""

- - - + + + - - - + + +