Compare commits

..

3 Commits

Author SHA1 Message Date
Simon Wisselink 721befc194 Documented all available modifiers 2024-02-26 12:06:29 +01:00
Simon Wisselink 9ef066fa85 WIP. Added split and join in favor of explode and implode modifiers. Updated docs. 2024-02-25 23:46:52 +01:00
Simon Wisselink e2494406c2 Implemented support for substr, implode and json_encode as modifiers.
Fixes #939
2024-02-25 13:11:58 +01:00
29 changed files with 217 additions and 157 deletions
-16
View File
@@ -6,31 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [5.0.0] - 2024-03-25
- Fixed that scoped variables would overwrite parent scope [#952](https://github.com/smarty-php/smarty/issues/952)
- Removed publicly accessible `$tpl->_var_stack` variable
### Fixed
- Too many shorthand attributes error when using a modifier as a function with more than 3 parameters in an expression [#949](https://github.com/smarty-php/smarty/issues/949)
### Removed
- Dropped support for undocumented `{time()}` added in v5.0.0 since we already have the documented `{$smarty.now}`
## [5.0.0-rc3] - 2024-02-26
### Added
- PHP8.3 support [#925](https://github.com/smarty-php/smarty/issues/925)
- Backlink to GitHub in docs
- Explain how to do escaping and set-up auto-escaping in docs [#865](https://github.com/smarty-php/smarty/issues/865)
- Link to variable scope page in the documentation for the assign tag [#878](https://github.com/smarty-php/smarty/issues/878)
- Add support for implode, substr and json_encode as modifiers/functions in templates [#939](https://github.com/smarty-php/smarty/issues/939)
- Add template path to CompilerException to enable rich debug features [#935](https://github.com/smarty-php/smarty/issues/935)
### Fixed
- The {debug} tag was broken in v5 [#922](https://github.com/smarty-php/smarty/issues/922)
- Documentation on `{if $x is even by $y}` syntax
- Fix incorrect compilation of expressions when escape_html=true [#930](https://github.com/smarty-php/smarty/pull/930)
## [5.0.0-rc2] - 2023-11-11
+1
View File
@@ -0,0 +1 @@
- Fix incorrect compilation of expressions when escape_html=true [#930](https://github.com/smarty-php/smarty/pull/930)
+1
View File
@@ -0,0 +1 @@
- Add support for implode, substr and json_encode as modifiers/functions in templates [#939](https://github.com/smarty-php/smarty/issues/939)
@@ -88,6 +88,9 @@ Object chaining:
{$object->method1($x)->method2($y)}
Direct PHP function access:
{time()}
```
> **Note**
-1
View File
@@ -117,7 +117,6 @@ The following constants have been removed to prevent global side effects.
- Smarty now always runs in multibyte mode. Make sure you use the [PHP multibyte extension](https://www.php.net/manual/en/book.mbstring.php) in production for optimal performance.
- Generated `<script>` tags lo longer have deprecated `type="text/javascript"` or `language="Javascript"` attributes
- Smarty will throw a compiler exception instead of silently ignoring a modifier on a function call, like this: `{include|dot:"x-template-id" file="included.dot.tpl"}`
- The ::getFile() method of a CompilerException will now return the full path of the template being compiled, if possible. This used to be 'file:relative_dir/filename.tpl'.
## Upgrading from v3 to v4
+1 -1
View File
@@ -12,7 +12,7 @@ git checkout -b "release/$1"
php utilities/update-changelog.php $1
php utilities/update-smarty-version-number.php $1
git add changelog CHANGELOG.md src/Smarty.php
git add CHANGELOG.md src/Smarty.php
git commit -m "version bump"
git checkout master
+13 -9
View File
@@ -34,7 +34,7 @@ class FunctionCallCompiler extends Base {
*
* @var array
*/
protected $shorttag_order = [];
protected $shorttag_order = ['var1', 'var2', 'var3'];
/**
* Compiles code for the execution of a registered function
@@ -58,15 +58,19 @@ class FunctionCallCompiler extends Base {
$_paramsArray = $this->formatParamsArray($_attr);
$_params = 'array(' . implode(',', $_paramsArray) . ')';
try {
$value = array_shift($_attr);
$output = $compiler->compileModifier([array_merge([$function], $_attr)], $value);
} catch (\Smarty\CompilerException $e) {
if ($functionHandler = $compiler->getSmarty()->getFunctionHandler($function)) {
if ($functionHandler = $compiler->getSmarty()->getFunctionHandler($function)) {
// not cacheable?
$compiler->tag_nocache = $compiler->tag_nocache || !$functionHandler->isCacheable();
$output = "\$_smarty_tpl->getSmarty()->getFunctionHandler(" . var_export($function, true) . ")";
$output .= "->handle($_params, \$_smarty_tpl)";
} else {
$compiler->trigger_template_error("unknown function '{$function}'", null, true);
// not cacheable?
$compiler->tag_nocache = $compiler->tag_nocache || !$functionHandler->isCacheable();
$output = "\$_smarty_tpl->getSmarty()->getFunctionHandler(" . var_export($function, true) . ")";
$output .= "->handle($_params, \$_smarty_tpl)";
} else {
throw $e;
}
}
if (!empty($parameter['modifierlist'])) {
@@ -1,19 +0,0 @@
<?php
namespace Smarty\Compile\Modifier;
use Smarty\CompilerException;
/**
* Smarty is_array modifier plugin
*/
class IsArrayModifierCompiler extends Base {
public function compile($params, \Smarty\Compiler\Template $compiler) {
if (count($params) !== 1) {
throw new CompilerException("Invalid number of arguments for is_array. is_array expects exactly 1 parameter.");
}
return 'is_array(' . $params[0] . ')';
}
}
+2 -2
View File
@@ -75,7 +75,7 @@ class FunctionClose extends Base {
$output .= "foreach (\$params as \$key => \$value) {\n\$_smarty_tpl->assign(\$key, \$value);\n}\n";
$output .= "\$params = var_export(\$params, true);\n";
$output .= "echo \"/*%%SmartyNocache:{$compiler->getTemplate()->getCompiled()->nocache_hash}%%*/<?php ";
$output .= "\\\$_smarty_tpl->pushStack();\nforeach (\$params as \\\$key => \\\$value) {\n\\\$_smarty_tpl->assign(\\\$key, \\\$value);\n}\n?>";
$output .= "\\\$_smarty_tpl->getSmarty()->getRuntime('TplFunction')->saveTemplateVariables(\\\$_smarty_tpl, '{$_name}');\nforeach (\$params as \\\$key => \\\$value) {\n\\\$_smarty_tpl->assign(\\\$key, \\\$value);\n}\n?>";
$output .= "/*/%%SmartyNocache:{$compiler->getTemplate()->getCompiled()->nocache_hash}%%*/\";?>";
$compiler->getParser()->current_buffer->append_subtree(
$compiler->getParser(),
@@ -86,7 +86,7 @@ class FunctionClose extends Base {
);
$compiler->getParser()->current_buffer->append_subtree($compiler->getParser(), $_functionCode);
$output = "<?php echo \"/*%%SmartyNocache:{$compiler->getTemplate()->getCompiled()->nocache_hash}%%*/<?php ";
$output .= "\\\$_smarty_tpl->popStack();?>\n";
$output .= "\\\$_smarty_tpl->getSmarty()->getRuntime('TplFunction')->restoreTemplateVariables(\\\$_smarty_tpl, '{$_name}');?>\n";
$output .= "/*/%%SmartyNocache:{$compiler->getTemplate()->getCompiled()->nocache_hash}%%*/\";\n?>";
$output .= "<?php echo str_replace('{$compiler->getTemplate()->getCompiled()->nocache_hash}', \$_smarty_tpl->getCompiled()->nocache_hash ?? '', ob_get_clean());\n";
$output .= "}\n}\n";
+1 -1
View File
@@ -121,6 +121,6 @@ class CodeFrame
* @return string
*/
public function insertLocalVariables(): string {
return '$_smarty_current_dir = ' . var_export(dirname($this->_template->getSource()->getFilepath() ?? '.'), true) . ";\n";
return '$_smarty_current_dir = ' . var_export(dirname($this->_template->getSource()->getFilepath()), true) . ";\n";
}
}
+1 -6
View File
@@ -848,7 +848,7 @@ class Template extends BaseCompiler {
$e = new CompilerException(
$error_text,
0,
$this->template->getSource()->getFilepath() ?? $this->template->getSource()->getFullResourceName(),
$this->template->getSource()->getFullResourceName(),
$line
);
$e->source = trim(preg_replace('![\t\r\n]+!', ' ', $match[$line - 1]));
@@ -1369,11 +1369,6 @@ class Template extends BaseCompiler {
return $this->functionCallCompiler->compile($args, $this, $parameter, $base_tag, $base_tag);
}
public function compileModifierInExpression(string $function, array $_attr) {
$value = array_shift($_attr);
return $this->compileModifier([array_merge([$function], $_attr)], $value);
}
/**
* @return TemplateParser|null
*/
-30
View File
@@ -47,20 +47,6 @@ class Data
*/
public $config_vars = array();
/**
* This variable will hold a stack of template variables.
*
* @var null|array
*/
private $_var_stack = [];
/**
* This variable will hold a stack of config variables.
*
* @var null|array
*/
private $_config_stack = [];
/**
* Default scope for new variables
* @var int
@@ -507,20 +493,4 @@ class Data
public function setParent($parent): void {
$this->parent = $parent;
}
public function pushStack(): void {
$stackList = [];
foreach ($this->tpl_vars as $name => $variable) {
$stackList[$name] = clone $variable; // variables are stored in Variable objects
}
$this->_var_stack[] = $this->tpl_vars;
$this->tpl_vars = $stackList;
$this->_config_stack[] = $this->config_vars;
}
public function popStack(): void {
$this->tpl_vars = array_pop($this->_var_stack);
$this->config_vars = array_pop($this->_config_stack);
}
}
+4 -21
View File
@@ -2,8 +2,6 @@
namespace Smarty\Extension;
use Smarty\Exception;
class DefaultExtension extends Base {
private $modifiers = [];
@@ -29,7 +27,6 @@ class DefaultExtension extends Base {
case 'escape': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\EscapeModifierCompiler(); break;
case 'from_charset': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\FromCharsetModifierCompiler(); break;
case 'indent': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\IndentModifierCompiler(); break;
case 'is_array': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\IsArrayModifierCompiler(); break;
case 'isset': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\IssetModifierCompiler(); break;
case 'json_encode': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\JsonEncodeModifierCompiler(); break;
case 'lower': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\LowerModifierCompiler(); break;
@@ -60,7 +57,6 @@ class DefaultExtension extends Base {
case 'escape': return [$this, 'smarty_modifier_escape'];
case 'explode': return [$this, 'smarty_modifier_explode'];
case 'implode': return [$this, 'smarty_modifier_implode'];
case 'in_array': return [$this, 'smarty_modifier_in_array'];
case 'join': return [$this, 'smarty_modifier_join'];
case 'mb_wordwrap': return [$this, 'smarty_modifier_mb_wordwrap'];
case 'number_format': return [$this, 'smarty_modifier_number_format'];
@@ -91,8 +87,12 @@ class DefaultExtension extends Base {
case 'html_select_date': $this->functionHandlers[$functionName] = new \Smarty\FunctionHandler\HtmlSelectDate(); break;
case 'html_select_time': $this->functionHandlers[$functionName] = new \Smarty\FunctionHandler\HtmlSelectTime(); break;
case 'html_table': $this->functionHandlers[$functionName] = new \Smarty\FunctionHandler\HtmlTable(); break;
case 'in_array': $this->functionHandlers[$functionName] = new \Smarty\FunctionHandler\InArray(); break;
case 'is_array': $this->functionHandlers[$functionName] = new \Smarty\FunctionHandler\IsArray(); break;
case 'mailto': $this->functionHandlers[$functionName] = new \Smarty\FunctionHandler\Mailto(); break;
case 'math': $this->functionHandlers[$functionName] = new \Smarty\FunctionHandler\Math(); break;
case 'strlen': $this->functionHandlers[$functionName] = new \Smarty\FunctionHandler\Strlen(); break;
case 'time': $this->functionHandlers[$functionName] = new \Smarty\FunctionHandler\Time(); break;
}
return $this->functionHandlers[$functionName] ?? null;
@@ -570,23 +570,6 @@ class DefaultExtension extends Base {
return implode((string) ($separator ?? ''), (array) $values);
}
/**
* Smarty in_array modifier plugin
* Type: modifier
* Name: in_array
* Purpose: test if value is contained in an array
*
* @param mixed $needle
* @param array $array
* @param bool $strict
*
* @return bool
*/
public function smarty_modifier_in_array($needle, $array, $strict = false)
{
return in_array($needle, (array) $array, (bool) $strict);
}
/**
* Smarty join modifier plugin
* Type: modifier
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace Smarty\FunctionHandler;
use Smarty\Exception;
use Smarty\Template;
/**
* in_array(mixed $needle, array $haystack, bool $strict = false): bool
* Returns true if needle is found in the array, false otherwise
*/
class InArray extends Base {
public function handle($params, Template $template) {
$params = array_values($params ?? []);
if (count($params) < 2 || count($params) > 3) {
throw new Exception("Invalid number of arguments for in_array. in_arrays expects 2 or 3 parameters.");
}
// default to false, true if param 3 is set to true
$needle = $params[0];
$haystack = (array) $params[1];
$strict = count($params) == 3 && $params[2];
return in_array($needle, $haystack, $strict);
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace Smarty\FunctionHandler;
use Smarty\Exception;
use Smarty\Template;
/**
* is_array(mixed $value): bool
* Returns true if value is an array, false otherwise.
*/
class IsArray extends Base {
public function handle($params, Template $template) {
if (count($params) !== 1) {
throw new Exception("Invalid number of arguments for is_array. is_array expects exactly 1 parameter.");
}
return is_array(reset($params));
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace Smarty\FunctionHandler;
use Smarty\Exception;
use Smarty\Template;
/**
* Get string length
*
* strlen(string $string): int
*
* Returns length of the string on success, and 0 if the string is empty.
*/
class Strlen extends Base {
public function handle($params, Template $template) {
$params = array_values($params ?? []);
if (count($params) !== 1) {
throw new Exception("Invalid number of arguments for strlen. strlen expects exactly 1 parameter.");
}
return strlen((string) $params[0]);
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace Smarty\FunctionHandler;
use Smarty\Exception;
use Smarty\Template;
/**
* is_array(mixed $value): bool
* Returns true if value is an array, false otherwise.
*/
class Time extends Base {
public function handle($params, Template $template) {
if (count($params) > 0) {
throw new Exception("Invalid number of arguments for time. time expects no parameters.");
}
return time();
}
}
+1 -1
View File
@@ -2675,7 +2675,7 @@ public static $yy_action = array(
}
// line 1063 "src/Parser/TemplateParser.y"
public function yy_r148(){
$this->_retvalue = $this->compiler->compileModifierInExpression($this->yystack[$this->yyidx + -3]->minor, $this->yystack[$this->yyidx + -1]->minor);
$this->_retvalue = $this->compiler->compileFunctionCall($this->yystack[$this->yyidx + -3]->minor, $this->yystack[$this->yyidx + -1]->minor);
}
// line 1071 "src/Parser/TemplateParser.y"
public function yy_r149(){
+1 -1
View File
@@ -1061,7 +1061,7 @@ objectelement(res)::= PTR method(f). {
// function
//
function(res) ::= ns1(f) OPENP params(p) CLOSEP. {
res = $this->compiler->compileModifierInExpression(f, p);
res = $this->compiler->compileFunctionCall(f, p);
}
+46 -17
View File
@@ -26,26 +26,31 @@ class TplFunctionRuntime {
*/
public function callTemplateFunction(Template $tpl, $name, $params, $nocache) {
$funcParam = $tpl->tplFunctions[$name] ?? ($tpl->getSmarty()->tplFunctions[$name] ?? null);
if (!isset($funcParam)) {
throw new \Smarty\Exception("Unable to find template function '{$name}'");
}
if (!$tpl->caching || ($tpl->caching && $nocache)) {
$function = $funcParam['call_name'];
} else {
if (isset($funcParam['call_name_caching'])) {
$function = $funcParam['call_name_caching'];
} else {
if (isset($funcParam)) {
if (!$tpl->caching || ($tpl->caching && $nocache)) {
$function = $funcParam['call_name'];
} else {
if (isset($funcParam['call_name_caching'])) {
$function = $funcParam['call_name_caching'];
} else {
$function = $funcParam['call_name'];
}
}
if (function_exists($function)) {
$this->saveTemplateVariables($tpl, $name);
$function($tpl, $params);
$this->restoreTemplateVariables($tpl, $name);
return;
}
// try to load template function dynamically
if ($this->addTplFuncToCache($tpl, $name, $function)) {
$this->saveTemplateVariables($tpl, $name);
$function($tpl, $params);
$this->restoreTemplateVariables($tpl, $name);
return;
}
}
if (!function_exists($function) && !$this->addTplFuncToCache($tpl, $name, $function)) {
throw new \Smarty\Exception("Unable to find template function '{$name}'");
}
$tpl->pushStack();
$function($tpl, $params);
$tpl->popStack();
throw new \Smarty\Exception("Unable to find template function '{$name}'");
}
/**
@@ -141,4 +146,28 @@ class TplFunctionRuntime {
return false;
}
/**
* Save current template variables on stack
*
* @param \Smarty\Template $tpl
* @param string $name stack name
*/
public function saveTemplateVariables(Template $tpl, $name) {
$tpl->_var_stack[] =
['tpl' => $tpl->tpl_vars, 'config' => $tpl->config_vars, 'name' => "_tplFunction_{$name}"];
}
/**
* Restore saved variables into template objects
*
* @param \Smarty\Template $tpl
* @param string $name stack name
*/
public function restoreTemplateVariables(Template $tpl, $name) {
if (isset($tpl->_var_stack)) {
$vars = array_pop($tpl->_var_stack);
$tpl->tpl_vars = $vars['tpl'];
$tpl->config_vars = $vars['config'];
}
}
}
+1 -1
View File
@@ -53,7 +53,7 @@ class Smarty extends \Smarty\TemplateBase {
/**
* smarty version
*/
const SMARTY_VERSION = '5.0.0';
const SMARTY_VERSION = '5.0.0-rc2';
/**
* define caching modes
+4 -2
View File
@@ -645,7 +645,8 @@ class Template extends TemplateBase {
} else {
// After rendering a template, the tpl/config variables are reset, so the template can be re-used.
$this->pushStack();
$savedTplVars = $this->tpl_vars;
$savedConfigVars = $this->config_vars;
// Start output-buffering.
ob_start();
@@ -653,7 +654,8 @@ class Template extends TemplateBase {
$result = $this->render(false, $function);
// Restore the template to its previous state
$this->popStack();
$this->tpl_vars = $savedTplVars;
$this->config_vars = $savedConfigVars;
}
if (isset($errorHandler)) {
+2 -2
View File
@@ -271,11 +271,11 @@ class Source {
return $this->type . ':' . $this->name;
}
public function getFilepath(): ?string {
public function getFilepath(): string {
if ($this->handler instanceof FilePlugin) {
return $this->handler->getFilePath($this->name, $this->smarty, $this->isConfig);
}
return null;
return '.';
}
public function isConfig(): bool {
+7
View File
@@ -59,6 +59,13 @@ abstract class TemplateBase extends Data {
*/
public $tplFunctions = [];
/**
* When initialized to an (empty) array, this variable will hold a stack of template variables.
*
* @var null|array
*/
public $_var_stack = null;
/**
* @var Debug
*/
@@ -0,0 +1,20 @@
<?php
namespace UnitTests\TemplateSource\TagTests\PluginFunction;
class TimeTest extends \PHPUnit_Smarty {
public function setUp(): void {
$this->setUpSmarty(__DIR__);
}
public function testBasicSyntax() {
$this->assertStringMatchesFormat('%d', $this->smarty->fetch("string:{time()}"));
}
public function testInvalidParameters() {
$this->expectException(\Smarty\Exception::class);
$this->expectExceptionMessage('Invalid number of arguments');
$this->assertEquals("", $this->smarty->fetch("string:{time(3, 'foo')}"));
}
}
@@ -79,7 +79,7 @@ class PhpFunctionTest extends PHPUnit_Smarty
public function testEmpty3()
{
$this->smarty->disableSecurity();
$this->getSmarty()->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'pass', function ($v) { return $v; });
$this->getSmarty()->registerPlugin(\Smarty\Smarty::PLUGIN_FUNCTION, 'pass', function ($v) { return $v; });
$this->smarty->assign('var', array(true,
(int) 1,
(float) 0.1,
@@ -99,7 +99,7 @@ class PhpFunctionTest extends PHPUnit_Smarty
public function testEmpty4()
{
$this->smarty->disableSecurity();
$this->getSmarty()->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'pass', function ($v) { return $v; });
$this->getSmarty()->registerPlugin(\Smarty\Smarty::PLUGIN_FUNCTION, 'pass', function ($v) { return $v; });
$this->smarty->assign('var', new TestIsset());
$expected = ' true , false , false , true , true , true , false ';
$this->assertEquals($expected, $this->smarty->fetch('string:{strip}{if empty($var->isNull)} true {else} false {/IF}
@@ -325,13 +325,4 @@ class ScopeTest extends PHPUnit_Smarty
$this->smarty->assign('scope', 'none');
$r = $this->smarty->fetch('test_function_scope.tpl');
}
public function testFunctionScopeIsLocalByDefault()
{
$this->assertEquals(
'a',
$this->smarty->fetch('string:{function name=test}{$var="b"}{/function}{$var="a"}{test}{$var}')
);
}
}
@@ -1,16 +0,0 @@
<?php
class TooManyShorthandAttributes949Test extends PHPUnit_Smarty
{
public function testPregMatchAll() {
$smarty = new \Smarty\Smarty();
$smarty->registerPlugin('modifier', 'var_dump', 'var_dump');
$templateStr = "eval:{\$a = 'blah'}{\$b = array()}{if var_dump('', \$a, \$b, 2)|noprint}blah{else}nah{/if}";
$this->assertEquals(
'nah',
$smarty->fetch($templateStr)
);
}
}
@@ -29,6 +29,12 @@ function smarty_function_checkvar($params, \Smarty\Template $template)
if (in_array('template', $types) && $ptr instanceof Template) {
$output .= "#{$ptr->getSource()->name}:\${$var} =";
$output .= $ptr->hasVariable($var) ? preg_replace('/\s/', '', var_export($ptr->getValue($var), true)) : '>unassigned<';
$i = 0;
while (isset($ptr->_var_stack[ $i ])) {
$output .= "#{$ptr->_var_stack[ $i ]['name']} = ";
$output .= isset($ptr->_var_stack[ $i ][ 'tpl' ][$var]) ? preg_replace('/\s/', '', var_export($ptr->_var_stack[ $i ][ 'tpl' ][$var]->value, true)) : '>unassigned<';
$i ++;
}
$ptr = $ptr->parent;
} elseif (in_array('data', $types) && !($ptr instanceof Template || $ptr instanceof \Smarty\Smarty)) {
$output .= "#data:\${$var} =";