diff --git a/CHANGELOG.md b/CHANGELOG.md index 69d41e7aa..6fd2a6c69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- Using unregistered static class methods in expressions now also triggers a deprecation notice because we will drop support for this in the next major release [#813](https://github.com/smarty-php/smarty/issues/813) + ## [4.4.1] - 2024-02-26 - Fixed internal release-tooling diff --git a/lexer/smarty_internal_templateparser.y b/lexer/smarty_internal_templateparser.y index 620498765..ffc85bc06 100644 --- a/lexer/smarty_internal_templateparser.y +++ b/lexer/smarty_internal_templateparser.y @@ -785,6 +785,9 @@ value(res) ::= ns1(c)DOUBLECOLON static_class_access(s). { if (isset($this->smarty->registered_classes[c])) { res = $this->smarty->registered_classes[c].'::'.s[0].s[1]; } else { + trigger_error('Using unregistered static method "' . c.'::'.s[0] . '" in a template is deprecated and will be ' . + 'removed in a future release. Use Smarty::registerClass to explicitly register ' . + 'a class for access.', E_USER_DEPRECATED); res = c.'::'.s[0].s[1]; } } else { diff --git a/libs/sysplugins/smarty_internal_compile_private_modifier.php b/libs/sysplugins/smarty_internal_compile_private_modifier.php index aea082f01..31fd6e1da 100644 --- a/libs/sysplugins/smarty_internal_compile_private_modifier.php +++ b/libs/sysplugins/smarty_internal_compile_private_modifier.php @@ -109,9 +109,11 @@ public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler, $ if (!is_object($compiler->smarty->security_policy) || $compiler->smarty->security_policy->isTrustedPhpModifier($modifier, $compiler) ) { - trigger_error('Using php-function "' . $modifier . '" as a modifier is deprecated and will be ' . - 'removed in a future release. Use Smarty::registerPlugin to explicitly register ' . - 'a custom modifier.', E_USER_DEPRECATED); + if (!in_array($modifier, ['time', 'join', 'is_array', 'in_array'])) { + trigger_error('Using unregistered function "' . $modifier . '" in a template is deprecated and will be ' . + 'removed in a future release. Use Smarty::registerPlugin to explicitly register ' . + 'a custom modifier.', E_USER_DEPRECATED); + } $output = "{$modifier}({$params})"; } $compiler->known_modifier_type[ $modifier ] = $type; diff --git a/libs/sysplugins/smarty_internal_templatecompilerbase.php b/libs/sysplugins/smarty_internal_templatecompilerbase.php index d5c18d31a..10caf5906 100644 --- a/libs/sysplugins/smarty_internal_templatecompilerbase.php +++ b/libs/sysplugins/smarty_internal_templatecompilerbase.php @@ -640,7 +640,17 @@ public function compilePHPFunctionCall($name, $parameter) return $func_name . '(' . $parameter[ 0 ] . ')'; } } else { - return $name . '(' . implode(',', $parameter) . ')'; + $first_param = array_shift($parameter); + $modifier = array_merge(array($name), $parameter); + // Now, compile the function call as a modifier + return $this->compileTag( + 'private_modifier', + array(), + array( + 'modifierlist' => array($modifier), + 'value' => $first_param + ) + ); } } else { $this->trigger_template_error("unknown function '{$name}'"); diff --git a/libs/sysplugins/smarty_internal_templateparser.php b/libs/sysplugins/smarty_internal_templateparser.php index a2dd0d6fb..c37d3c187 100644 --- a/libs/sysplugins/smarty_internal_templateparser.php +++ b/libs/sysplugins/smarty_internal_templateparser.php @@ -2425,6 +2425,9 @@ public function yy_r99(){ if (isset($this->smarty->registered_classes[$this->yystack[$this->yyidx + -2]->minor])) { $this->_retvalue = $this->smarty->registered_classes[$this->yystack[$this->yyidx + -2]->minor].'::'.$this->yystack[$this->yyidx + 0]->minor[0].$this->yystack[$this->yyidx + 0]->minor[1]; } else { + trigger_error('Using unregistered static method "' . $this->yystack[$this->yyidx + -2]->minor.'::'.$this->yystack[$this->yyidx + 0]->minor[0] . '" in a template is deprecated and will be ' . + 'removed in a future release. Use Smarty::registerClass to explicitly register ' . + 'a class for access.', E_USER_DEPRECATED); $this->_retvalue = $this->yystack[$this->yyidx + -2]->minor.'::'.$this->yystack[$this->yyidx + 0]->minor[0].$this->yystack[$this->yyidx + 0]->minor[1]; } } else { diff --git a/libs/sysplugins/smarty_security.php b/libs/sysplugins/smarty_security.php index 97cd0521d..49ae2a386 100644 --- a/libs/sysplugins/smarty_security.php +++ b/libs/sysplugins/smarty_security.php @@ -253,7 +253,7 @@ public function __construct($smarty) * * @param string $function_name * @param object $compiler compiler object - * + * @deprecated * @return boolean true if function is trusted */ public function isTrustedPhpFunction($function_name, $compiler) diff --git a/phpunit.xml b/phpunit.xml index e0ad81d69..15e1d5630 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -15,7 +15,11 @@ timeoutForSmallTests="1" timeoutForMediumTests="10" timeoutForLargeTests="60" - verbose="false"> + verbose="false" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + convertDeprecationsToExceptions="true"> ./tests/UnitTests/ diff --git a/run-tests-for-all-php-versions.sh b/run-tests-for-all-php-versions.sh index 79bebb8a6..23541b519 100755 --- a/run-tests-for-all-php-versions.sh +++ b/run-tests-for-all-php-versions.sh @@ -5,11 +5,13 @@ # - ./run-tests-for-all-php-versions.sh --group 20221124 # - ./run-tests-for-all-php-versions.sh --exclude-group slow -docker-compose run php71 ./run-tests.sh $@ && \ -docker-compose run php72 ./run-tests.sh $@ && \ -docker-compose run php73 ./run-tests.sh $@ && \ -docker-compose run php74 ./run-tests.sh $@ && \ -docker-compose run php80 ./run-tests.sh $@ && \ -docker-compose run php81 ./run-tests.sh $@ && \ -docker-compose run php82 ./run-tests.sh $@ && \ -docker-compose run php83 ./run-tests.sh $@ +COMPOSE_CMD="mutagen-compose" + +$COMPOSE_CMD run --rm php71 ./run-tests.sh $@ && \ +$COMPOSE_CMD run --rm php72 ./run-tests.sh $@ && \ +$COMPOSE_CMD run --rm php73 ./run-tests.sh $@ && \ +$COMPOSE_CMD run --rm php74 ./run-tests.sh $@ && \ +$COMPOSE_CMD run --rm php80 ./run-tests.sh $@ && \ +$COMPOSE_CMD run --rm php81 ./run-tests.sh $@ && \ +$COMPOSE_CMD run --rm php82 ./run-tests.sh $@ && \ +$COMPOSE_CMD run --rm php83 ./run-tests.sh $@ diff --git a/tests/PHPUnit_Smarty.php b/tests/PHPUnit_Smarty.php index de4a37769..bed598185 100644 --- a/tests/PHPUnit_Smarty.php +++ b/tests/PHPUnit_Smarty.php @@ -149,7 +149,7 @@ public function setUpSmarty($dir = null) $this->smarty->setCacheDir(__DIR__ . '/cache'); } - $this->getSmartyObj(); + $this->smarty->setErrorReporting(E_ALL &~ E_USER_DEPRECATED); } /** diff --git a/tests/UnitTests/A_Core/LoadPlugin/LoadPluginTest.php b/tests/UnitTests/A_Core/LoadPlugin/LoadPluginTest.php index 820f31a46..27f90a034 100644 --- a/tests/UnitTests/A_Core/LoadPlugin/LoadPluginTest.php +++ b/tests/UnitTests/A_Core/LoadPlugin/LoadPluginTest.php @@ -49,6 +49,6 @@ public function testLoadPluginSmartyTemplateClass() */ public function testLoadPluginSmartyPluginCounter() { - $this->assertTrue($this->smarty->loadPlugin('Smarty_Function_Counter') == true); + $this->assertTrue($this->smarty->loadPlugin('Smarty_function_counter') == true); } } diff --git a/tests/UnitTests/SecurityTests/SecurityTest.php b/tests/UnitTests/SecurityTests/SecurityTest.php index 7631a0a6b..c3398f954 100644 --- a/tests/UnitTests/SecurityTests/SecurityTest.php +++ b/tests/UnitTests/SecurityTests/SecurityTest.php @@ -52,7 +52,7 @@ public function testSecurityLoaded() */ public function testTrustedPHPFunction() { - $this->assertEquals("5", $this->smarty->fetch('string:{assign var=foo value=[1,2,3,4,5]}{sizeof($foo)}')); + $this->assertEquals("5", $this->smarty->fetch('string:{assign var=foo value=[1,2,3,4,5]}{count($foo)}')); } /** @@ -75,7 +75,7 @@ public function testDisabledTrustedPHPFunction() { $this->smarty->security_policy->php_functions = array('null'); $this->smarty->disableSecurity(); - $this->assertEquals("5", $this->smarty->fetch('string:{assign var=foo value=[1,2,3,4,5]}{sizeof($foo)}')); + $this->assertEquals("5", $this->smarty->fetch('string:{assign var=foo value=[1,2,3,4,5]}{count($foo)}')); } /** diff --git a/tests/UnitTests/SmartyMethodsTests/ClearAllAssign/ClearAllAssignTest.php b/tests/UnitTests/SmartyMethodsTests/ClearAllAssign/ClearAllAssignTest.php index 9363f323f..5c1a679ff 100644 --- a/tests/UnitTests/SmartyMethodsTests/ClearAllAssign/ClearAllAssignTest.php +++ b/tests/UnitTests/SmartyMethodsTests/ClearAllAssign/ClearAllAssignTest.php @@ -46,7 +46,7 @@ public function testAllVariablesAccessable() */ public function testClearAllAssignInTemplate() { - error_reporting((error_reporting() & ~(E_NOTICE | E_USER_NOTICE | E_WARNING))); + $this->smarty->setErrorReporting($this->smarty->error_reporting & ~(E_NOTICE | E_USER_NOTICE | E_WARNING)); $this->_tpl->clearAllAssign(); $this->assertEquals('foobar', $this->smarty->fetch($this->_tpl)); } @@ -56,7 +56,7 @@ public function testClearAllAssignInTemplate() */ public function testClearAllAssignInData() { - error_reporting((error_reporting() & ~(E_NOTICE | E_USER_NOTICE | E_WARNING))); + $this->smarty->setErrorReporting($this->smarty->error_reporting & ~(E_NOTICE | E_USER_NOTICE | E_WARNING)); $this->_data->clearAllAssign(); $this->assertEquals('fooblar', $this->smarty->fetch($this->_tpl)); } @@ -66,7 +66,7 @@ public function testClearAllAssignInData() */ public function testClearAllAssignInSmarty() { - error_reporting((error_reporting() & ~(E_NOTICE | E_USER_NOTICE | E_WARNING))); + $this->smarty->setErrorReporting($this->smarty->error_reporting & ~(E_NOTICE | E_USER_NOTICE | E_WARNING)); $this->smarty->clearAllAssign(); $this->assertEquals('barblar', $this->smarty->fetch($this->_tpl)); } diff --git a/tests/UnitTests/TemplateSource/ValueTests/PHPfunctions/PhpFunctionTest.php b/tests/UnitTests/TemplateSource/ValueTests/PHPfunctions/PhpFunctionTest.php index cf8970c5d..9a3f63191 100644 --- a/tests/UnitTests/TemplateSource/ValueTests/PHPfunctions/PhpFunctionTest.php +++ b/tests/UnitTests/TemplateSource/ValueTests/PHPfunctions/PhpFunctionTest.php @@ -57,6 +57,7 @@ public function testEmpty1() public function testEmpty2() { $this->smarty->disableSecurity(); + $this->smarty->registerPlugin('modifier', 'pass', 'pass'); $this->smarty->assign('var', array(null, false, (int) 0, @@ -78,6 +79,7 @@ public function testEmpty2() public function testEmpty3() { $this->smarty->disableSecurity(); + $this->smarty->registerPlugin('modifier', 'pass', 'pass'); $this->smarty->assign('var', array(true, (int) 1, (float) 0.1, @@ -114,6 +116,7 @@ public function testEmpty4() public function testIsset1() { $this->smarty->disableSecurity(); + $this->smarty->registerPlugin('modifier', 'pass', 'pass'); $this->smarty->assign('isNull', null); $this->smarty->assign('isSet', 1); $this->smarty->assign('arr', array('isNull' => null, 'isSet' => 1)); @@ -155,7 +158,7 @@ public function testIsset2() public function testIsset3($strTemplate, $result) { $this->smarty->disableSecurity(); - + $this->smarty->registerPlugin('modifier', 'intval', 'intval'); $this->smarty->assign('varobject', new TestIsset()); $this->smarty->assign('vararray', $vararray = array( 'keythatexists' => false, @@ -196,6 +199,105 @@ public function dataTestIsset3() array('{if isset($_varsimple{$key})}true{else}false{/if}', 'true'), ); } + + /** + * Tests various PHP functions (deprecated) + * @dataProvider dataVariousPHPFunctions + */ + public function testVariousPHPFunctions($strTemplate, $value, $expected) { + $this->smarty->disableSecurity(); + $this->cleanDirs(); + $this->smarty->assign('value', $value); + $this->assertEquals($expected, $this->smarty->fetch('string:' . $strTemplate)); + } + + /** + * Data provider for testIsset3 + */ + public function dataVariousPHPFunctions() + { + return array( + array('{$a = count($value)}{$a}', array(1,2,3), '3'), + array('{$a = in_array("b", $value)}{$a}', array(1,'b',3), true), + array('{$a = strlen(uniqid())}{$a}', '', 13), + array('{$a = date("Y", $value)}{$a}', strtotime("01-01-2030"), 2030), + array('{$a = PhpFunctionTest::sayHi($value)}{$a}', 'mario', 'hi mario'), + array('{$a = pass($value)}{$a}', 'mario', 'mario'), + array('{$a = 1}{$b = Closure::fromCallable($value)}{$a}', 'strlen', 1), + ); + } + + public static function sayHi($value) { + return 'hi ' . $value; + } + + /** + * Tests that each function that will not be supported in Smarty 5 does throw an E_USER_DEPRECATED notice. + * @dataProvider dataDeprecationNoticesForSmarty5 + * @deprecated + */ + public function testDeprecationNoticesForSmarty5($strTemplateSource, $expected = '', $shouldTriggerDeprecationNotice = false) { + + // this overrides the error reporting level set in \PHPUnit_Smarty::setUpSmarty + $previousErrorReporting = $this->smarty->error_reporting; + $this->smarty->setErrorReporting($previousErrorReporting | E_USER_DEPRECATED); + + $this->smarty->assign('a', 'a'); + $this->smarty->assign('ar', [1,2]); + $this->smarty->assign('f', 3.14); + + $errorMessage = ''; + + try { + $output = $this->smarty->fetch('string:' . $strTemplateSource); + } catch (Exception $e) { + $errorMessage = $e->getMessage(); + } + + if ($shouldTriggerDeprecationNotice) { + $this->assertStringContainsString('Using unregistered function', $errorMessage); + } else { + $this->assertEquals($expected, $output); + $this->assertEquals('', $errorMessage); + } + + $this->smarty->setErrorReporting($previousErrorReporting); + } + + public function dataDeprecationNoticesForSmarty5() + { + + return [ + + ['{if empty($a)}{else}b{/if}', 'b', false], + ['{json_encode($a)}', '"a"', false], + ['{nl2br($a)}', 'a', false], + ['{$a|nl2br}', 'a', false], + ['{round($f, 1)}', '3.1', false], + ['{$f|round}', '3', false], + ['{str_repeat($a, 2)}', 'aa', false], + ['{$a|str_repeat:3}', 'aaa', false], + ['{$a|strip_tags}', 'a', false], + ['{$a|strlen}', '1', false], + ['{$a|substr:-1}', 'a', false], + ['{$f|substr:-1}', '4', false], + ['{$ar|count}', '2', false], + ['{foreach "."|explode:$f as $n}{$n}{/foreach}', '314', false], + ['{"-"|implode:$ar}', '1-2', false], + ['{"-"|join:$ar}', '1-2', false], + ['{$f|wordwrap:2:"k":true}', "3.k14", false], + ['{$f|number_format:1:","}', "3,1", false], + ['{if in_array(1, $ar)}yes{/if}', "yes", false], + ['{if is_array($ar)}yes{/if}', "yes", false], + ['{if time() gt 0}yes{/if}', "yes", false], + + ['{if array_chunk($ar, 2)}x{else}y{/if}', '', true], + ['{$a|addslashes}', '', true], + ['{$a|sha1}', '', true], + ['{$a|get_parent_class}', '', true], + ]; + } + } /**