Compare commits

..

10 Commits

Author SHA1 Message Date
Simon Wisselink b08a1d8332 add changelog 2026-04-13 21:35:41 +02:00
Simon Wisselink a511a011cb changed an incorrect doc and formatted some code. 2026-04-13 15:13:28 +02:00
Simon Wisselink 7cab1c0c24 remove useless resetting of static properties in tearDownAfterClass 2026-04-13 14:15:34 +02:00
Simon Wisselink b7dac0306f Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-13 14:08:08 +02:00
Simon Wisselink e73aaf19b5 fixed the unit tests 2026-04-13 13:58:09 +02:00
Simon Wisselink 06c49acee4 cleanup of unused template files, non-shared files stored in __shared folder, no longer required calls to add template folders et cetera 2026-04-13 13:17:20 +02:00
Simon Wisselink c22d109d17 Clean up each test class's unique temp dir in tearDownAfterClass()
Add a private static removeDir() helper and call it from
tearDownAfterClass() to recursively delete the per-class unique temp
directory after each test class finishes. Cleanup failures are silently
ignored (@ suppression) so they never cause test failures.

Set KEEP_SMARTY_TEST_ARTIFACTS=1 in the environment to skip cleanup and
keep the artifacts on disk for debugging.
2026-04-13 11:10:26 +02:00
Simon Wisselink deb0b22976 Remove individualFolders dead code and spurious assertTrue from cleanDirs()
- Remove the never-active individualFolders code path from setUpSmarty()
  (the constant was always true, making the branch unreachable)
- Remove define('individualFolders') from Config.php and the constructor
- Remove $this->assertTrue(true) from cleanDirs(): it existed solely to
  make testInit() count as a passing test; now that cleanDirs() is called
  from setUpSmarty() and from test methods directly, the assertion was
  spuriously inflating assertion counts
- Add tests/**/templates_c/, cache/, templates_tmp/ to .gitignore to
  prevent stale test output from appearing as untracked files
2026-04-13 00:40:28 +02:00
Simon Wisselink cc3e9c2a20 Isolate each test class in a unique temp directory
getTempDir() now appends a per-class uniqid token to the temp path, so
concurrent or sequential test runs never share compiled/cached output.
The token is generated lazily on first use and reset in
tearDownAfterClass(), giving every test class a fresh isolated directory.

As a result, the Bootstrap.php pre-run cleanup of smarty-tests/ is no
longer needed for correctness (stale paths are unreachable) and was
harmful to concurrent runs, so it has been removed.
2026-04-12 23:53:51 +02:00
Simon Wisselink 760f4834b3 Redirect test temp dirs to system temp directory. Fixes #1178
Move all test-generated output (compiled templates, cache files, and
temporary template sources) from per-test-directory folders inside the
working tree to a parallel structure under sys_get_temp_dir()/smarty-tests/.

This removes 215 boilerplate .gitignore files from the repo and ensures
running the test suite leaves zero uncommitted files in the working tree.

All 2296 tests continue to pass with identical behavior.
2026-04-11 00:05:42 +02:00
40 changed files with 117 additions and 767 deletions
+1 -13
View File
@@ -1,13 +1,9 @@
# AGENTS.md
This file is the single source of truth for AI coding assistants working in this repo (including Claude Code, claude.ai/code).
## Project
Smarty v5 — PHP template engine. Single Composer package (`smarty/smarty`), namespace `Smarty\`, source in `src/`, autoloaded via PSR-4. Supports PHP 7.28.5.
Do not use PHP syntax newer than 7.2 in `src/` unless it is guarded for older runtimes.
## Commands
```bash
@@ -63,7 +59,6 @@ After editing a `.plex` or `.y` file, run `make -B` to regenerate. The generator
- All tests extend `PHPUnit_Smarty` (defined in `tests/PHPUnit_Smarty.php`), which provides `setUpSmarty($dir)`.
- Test suite root: `tests/UnitTests/`. Typical test `setUp()` calls `$this->setUpSmarty(__DIR__)`.
- Each test directory may have its own `templates/`, `configs/` subdirectories. Compiled output goes to `templates_c/` and `cache/` (auto-created by the test harness).
- Running the suite scatters generated `templates_c/`, `cache/`, and `templates_tmp/` directories (and empty runtime `templates/`/`configs/` dirs) throughout `tests/` and the repo root. These are not tracked — treat them as noise in `git status`, never commit them, and clean them with `git clean -fd` (exclude tool dirs like `.serena`).
- Three test files are excluded in `phpunit.xml`: Memcache, APC, and HttpModifiedSince tests (require external services).
- Tests needing MySQL/PDO are gated by constants in `tests/Config.php` (disabled by default).
@@ -73,15 +68,8 @@ GitHub Actions (`.github/workflows/ci.yml`): matrix of PHP 7.28.5 on ubuntu +
## Docs
Markdown in `docs/`, built with mkdocs + Material theme. Install the toolchain with `pip install -r docs/requirements.txt`, then preview with `mkdocs serve`. Published via `mike deploy 5.x`.
Markdown in `docs/`, built with mkdocs + Material theme. Preview: `mkdocs serve`. Published via `mike deploy 5.x`.
## Release
`./make-release.sh <version>` — only v5.x.x. Updates changelog and version constant, creates a merge commit and tag on `master`.
### Changelog
Every change that should appear in `CHANGELOG.md` must add a new markdown file under `changelog/`. At release time `utilities/update-changelog.php` concatenates all `changelog/*.md` files into the `## [Unreleased]` section, so:
- One file per change, containing a single line that starts with a dash (`- ...`). The filename is arbitrary; name it after the issue number (e.g. `1036.md`) when there is one, otherwise use a short descriptive slug.
- Include a markdown link to the relevant issue when one exists, e.g. `[#1036](https://github.com/smarty-php/smarty/issues/1036)`. Omit the link when there is no public issue (e.g. embargoed security reports).
-22
View File
@@ -6,28 +6,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [5.8.4] - 2026-06-29
- Fixed a `TypeError` on PHP 8 when `Security::$static_classes` was set to a non-array value (e.g. the string `'none'`) to disable static class access; any non-array value now cleanly denies access. Use `Security::$static_classes = null` to disable access to all static classes.
- Security: the built-in `stream:` resource type now validates the nested stream wrapper against the security policy, so a template such as `stream:php://filter/...` can no longer bypass `Security::$streams` (including `Security::$streams = null`) to read local files (CWE-22)
## [5.8.3] - 2026-06-28
- fixed a regression from #1189 where a child template's block override no longer applied to a template {include}d by the parent [#1192](https://github.com/smarty-php/smarty/issues/1192)
## [5.8.2] - 2026-06-24
- Security: prevent symlinks inside a trusted `secure_dir`/template directory from being used to read files outside of it (CWE-22 path traversal), affecting `{include}` and `{fetch}` of local files
- Security: `{html_image}` now escapes the `file`, `path_prefix`, `href`/`link`, `width` and `height` attributes (it already escaped `alt` and pass-through attributes), and `{html_select_date}` casts `day_size`/`month_size`/`year_size` to int (matching `{html_select_time}`), preventing untrusted values passed into these attributes from breaking out of the generated HTML (CWE-79)
- Security: `{fetch}` no longer follows HTTP redirects for remote resources while a security policy is active, preventing an open redirect on a trusted host from bypassing `trusted_uri` (CWE-918 server-side request forgery)
- Fixed "Attempt to assign property step on null" error when using a {for} loop inside a block of an extended template [#1036](https://github.com/smarty-php/smarty/issues/1036)
## [5.8.1] - 2026-06-23
- Re-activated unit tests for user literals, which were previously disabled due to a bug in refactoring to v5.
- fixed a bug where child template's block content leaked into subsequent rendering of the parent template [#1189](https://github.com/smarty-php/smarty/issues/1189)
- Moved all unit test-generated output from inside the working tree to tmp files [#1178](https://github.com/smarty-php/smarty/issues/1178)
## [5.8.0] - 2026-02-15
- Added support for Backed Enums for php versions >= 8.1 [#1171](https://github.com/smarty-php/smarty/pull/1171)
- Added support for new 'matches' operator doing regex matching [#1169](https://github.com/smarty-php/smarty/pull/1169)
-5
View File
@@ -100,11 +100,6 @@ Enhancement suggestions are tracked as [GitHub issues](https://github.com/smarty
The [docs](docs/index.md) are written in markdown, configured in [mkdocs.yml](mkdocs.yml) and published
to [GitHub pages](https://smarty-php.github.io/smarty) using [mkdocs](https://www.mkdocs.org/) and [mike](https://github.com/jimporter/mike).
You need Python to build the docs. Install the required packages first:
```bash
pip install -r docs/requirements.txt
```
To preview the docs while you are writing, run:
```bash
mkdocs serve
+5 -4
View File
@@ -11,10 +11,10 @@
## include inline
- Re-introduce merge_compiled_includes and the {include inline} attribute?
## Output buffering (major)
- Fix ob_ output buffering commands being scattered around the codebase: Smarty's output model is fundamentally "echo everything, wrap in a buffer to capture". An alternative that would be where rendering returns a string rather than echoing — but that touches the entire compiled template format (the unifunc functions all echo) and is a large change.
## Output buffering
- Fix ob_ output buffering commands being scattered around the codebase
## Review public static vars (major)
## Review public static vars
- such as _CHARSET and _IS_WINDOWS
## Block / inheritance
@@ -24,8 +24,9 @@
## Plugin system
- fix template security checks in one place in compiler
## Beatify output (major)
## Beatify output
- compiled templates could be proper classes, possibly using [nette/php-generator](https://packagist.org/packages/nette/php-generator)
## Unrelated / other
- review (and avoid) use of 'clone' keyword
- what is 'user literal support', why are unit tests skipped?
+1
View File
@@ -0,0 +1 @@
- Moved all unit test-generated output from inside the working tree to tmp files [#1178](https://github.com/smarty-php/smarty/issues/1178)
+1
View File
@@ -1,3 +1,4 @@
version: "2"
services:
base:
build:
@@ -27,13 +27,6 @@ which item(s) are selected by default as well.
- All parameters that are not in the list above are printed as
name/value-pairs inside each of the created <input\>-tags.
> **Security note**
>
> The `separator` attribute is written into the generated HTML without escaping,
> so it can contain markup such as `separator='<br />'`. If its value originates
> from untrusted input, escape it yourself first to avoid cross-site scripting
> (XSS). Option values and labels are escaped automatically.
## Examples
```php
<?php
@@ -28,13 +28,6 @@ selected by default as well.
- All parameters that are not in the list above are output as
name/value-pairs inside each of the created `<input>`-tags.
> **Security note**
>
> The `separator` attribute is written into the generated HTML without escaping,
> so it can contain markup such as `separator='<br />'`. If its value originates
> from untrusted input, escape it yourself first to avoid cross-site scripting
> (XSS). Option values and labels are escaped automatically.
## Examples
```php
@@ -47,14 +47,6 @@ name/value-pairs inside the `<select>` tags of day, month and year.
> There is an useful php function on the [date tips page](../../appendixes/tips.md)
> for converting `{html_select_date}` form values to a timestamp.
> **Security note**
>
> The `*_extra` attributes, `field_separator`/`option_separator`, and any
> unrecognised parameter (which is emitted as a raw attribute on the `<select>`
> tag) are written into the generated HTML without escaping. If any of these
> values originate from untrusted input, escape them yourself first to avoid
> cross-site scripting (XSS).
## Exaples
Template code
@@ -47,14 +47,6 @@ parseable by PHP's [`strtotime()`](https://www.php.net/strtotime).
| meridian\_empty | null | If supplied then the first element of the meridian's select-box has this value as it's label and "" as it's value. This is useful to make the select-box read "Please select an meridian" for example. |
> **Security note**
>
> The `*_extra` attributes, `field_separator`/`option_separator`, and any
> unrecognised parameter (which is emitted as a raw attribute on the `<select>`
> tag) are written into the generated HTML without escaping. If any of these
> values originate from untrusted input, escape them yourself first to avoid
> cross-site scripting (XSS).
## Examples
```smarty
@@ -31,15 +31,6 @@ dumps an array of data into an HTML `<table>`.
- `trailpad` is the value put into the trailing cells on the last
table row if there are any present.
> **Security note**
>
> The `loop`/`cols` data and the `caption`, `trailpad`, `table_attr`, `tr_attr`,
> `td_attr` and `th_attr` attributes are written into the generated HTML without
> escaping (this is by design — e.g. `table_attr='border="1"'`). If any of these
> values originate from untrusted input, escape them yourself first (e.g. with the
> [`escape`](../language-modifiers/language-modifier-escape.md) modifier) to avoid
> cross-site scripting (XSS).
## Examples
```php
@@ -24,14 +24,6 @@ spiders to lift email addresses off of a site.
> you can use hex encoding too.
> **Security note**
>
> The `extra` attribute is written into the generated `<a>` tag without escaping,
> so that you can add attributes such as `extra='class="mailto"'`. If you pass a
> value that originates from untrusted input, escape it yourself first (e.g. with
> the [`escape`](../language-modifiers/language-modifier-escape.md) modifier) to
> avoid cross-site scripting (XSS).
## Examples
```smarty
-14
View File
@@ -1,14 +0,0 @@
# Python dependencies for building/previewing the docs.
#
# pip install -r docs/requirements.txt
# mkdocs serve # local preview
# mike deploy 5.x # publish
#
# pymdown-extensions must be >=11: earlier releases pass filename=None to
# Pygments, and Pygments >=2.19 then crashes with
# "'NoneType' object has no attribute 'replace'" on any untitled code block.
mkdocs>=1.6
mkdocs-material>=9.7
pymdown-extensions>=11
Pygments>=2.19
mike>=2.2
+1
View File
@@ -20,6 +20,7 @@
<testsuite name="foo">
<directory>./tests/UnitTests/</directory>
<exclude>./tests/UnitTests/CacheResourceTests/Memcache/CacheResourceCustomMemcacheTest.php</exclude>
<exclude>./tests/UnitTests/CacheResourceTests/Apc/CacheResourceCustomApcTest.php</exclude>
<exclude>./tests/UnitTests/CacheModify/ModifiedSince/HttpModifiedSinceTest.php</exclude>
</testsuite>
</testsuites>
+1 -1
View File
@@ -5,7 +5,7 @@
# - ./run-tests-for-all-php-versions.sh --group 20221124
# - ./run-tests-for-all-php-versions.sh --exclude-group slow
COMPOSE_CMD="docker compose"
COMPOSE_CMD="mutagen-compose"
$COMPOSE_CMD run --rm php72 ./run-tests.sh $@ && \
$COMPOSE_CMD run --rm php73 ./run-tests.sh $@ && \
+1 -1
View File
@@ -143,7 +143,7 @@ class File extends Base
*
* @param Template $_template template object
*
* @return string|false content
* @return string content
*/
public function retrieveCachedContent(Template $_template)
{
+12 -15
View File
@@ -51,9 +51,8 @@ class ForTag extends Base {
$var = $_statement['var'];
$index = '';
}
$itemVar = "\$_smarty_tpl->getVariable({$var})";
$output .= "\$_smarty_tpl->assign($var, []);\n";
$output .= "{$itemVar}->value{$index} = {$_statement['value']};\n";
$output .= "\$_smarty_tpl->assign($var, null);\n";
$output .= "\$_smarty_tpl->tpl_vars[$var]->value{$index} = {$_statement['value']};\n";
}
if (is_array($_attr['var'])) {
$var = $_attr['var']['var'];
@@ -62,8 +61,7 @@ class ForTag extends Base {
$var = $_attr['var'];
$index = '';
}
$itemVar = "\$_smarty_tpl->getVariable({$var})";
$output .= "if ($_attr[ifexp]) {\nfor (\$_foo=true;$_attr[ifexp]; {$itemVar}->value{$index}$_attr[step]) {\n";
$output .= "if ($_attr[ifexp]) {\nfor (\$_foo=true;$_attr[ifexp]; \$_smarty_tpl->tpl_vars[$var]->value{$index}$_attr[step]) {\n";
} else {
$_statement = $_attr['start'];
if (is_array($_statement['var'])) {
@@ -73,22 +71,21 @@ class ForTag extends Base {
$var = $_statement['var'];
$index = '';
}
$itemVar = "\$_smarty_tpl->getVariable({$var})";
$output .= "\$_smarty_tpl->assign($var, []);";
$output .= "\$_smarty_tpl->assign($var, null);";
if (isset($_attr['step'])) {
$output .= "{$itemVar}->step = $_attr[step];";
$output .= "\$_smarty_tpl->tpl_vars[$var]->step = $_attr[step];";
} else {
$output .= "{$itemVar}->step = 1;";
$output .= "\$_smarty_tpl->tpl_vars[$var]->step = 1;";
}
if (isset($_attr['max'])) {
$output .= "{$itemVar}->total = (int) min(ceil(({$itemVar}->step > 0 ? $_attr[to]+1 - ($_statement[value]) : $_statement[value]-($_attr[to])+1)/abs({$itemVar}->step)),$_attr[max]);\n";
$output .= "\$_smarty_tpl->tpl_vars[$var]->total = (int) min(ceil((\$_smarty_tpl->tpl_vars[$var]->step > 0 ? $_attr[to]+1 - ($_statement[value]) : $_statement[value]-($_attr[to])+1)/abs(\$_smarty_tpl->tpl_vars[$var]->step)),$_attr[max]);\n";
} else {
$output .= "{$itemVar}->total = (int) ceil(({$itemVar}->step > 0 ? $_attr[to]+1 - ($_statement[value]) : $_statement[value]-($_attr[to])+1)/abs({$itemVar}->step));\n";
$output .= "\$_smarty_tpl->tpl_vars[$var]->total = (int) ceil((\$_smarty_tpl->tpl_vars[$var]->step > 0 ? $_attr[to]+1 - ($_statement[value]) : $_statement[value]-($_attr[to])+1)/abs(\$_smarty_tpl->tpl_vars[$var]->step));\n";
}
$output .= "if ({$itemVar}->total > 0) {\n";
$output .= "for ({$itemVar}->value{$index} = $_statement[value], {$itemVar}->iteration = 1;{$itemVar}->iteration <= {$itemVar}->total;{$itemVar}->value{$index} += {$itemVar}->step, {$itemVar}->iteration++) {\n";
$output .= "{$itemVar}->first = {$itemVar}->iteration === 1;";
$output .= "{$itemVar}->last = {$itemVar}->iteration === {$itemVar}->total;";
$output .= "if (\$_smarty_tpl->tpl_vars[$var]->total > 0) {\n";
$output .= "for (\$_smarty_tpl->tpl_vars[$var]->value{$index} = $_statement[value], \$_smarty_tpl->tpl_vars[$var]->iteration = 1;\$_smarty_tpl->tpl_vars[$var]->iteration <= \$_smarty_tpl->tpl_vars[$var]->total;\$_smarty_tpl->tpl_vars[$var]->value{$index} += \$_smarty_tpl->tpl_vars[$var]->step, \$_smarty_tpl->tpl_vars[$var]->iteration++) {\n";
$output .= "\$_smarty_tpl->tpl_vars[$var]->first = \$_smarty_tpl->tpl_vars[$var]->iteration === 1;";
$output .= "\$_smarty_tpl->tpl_vars[$var]->last = \$_smarty_tpl->tpl_vars[$var]->iteration === \$_smarty_tpl->tpl_vars[$var]->total;";
}
$output .= '?>';
+1 -16
View File
@@ -189,22 +189,7 @@ class Fetch extends Base {
return;
}
} else {
if ($protocol && isset($template->getSmarty()->security_policy)) {
// Remote resource (e.g. https://) reached through file_get_contents().
// isTrustedUri() only validates the initial URL, but file_get_contents()
// follows redirects by default, so an open redirect on an otherwise
// trusted host could be used to reach a non-trusted target (SSRF).
// Disable redirect-following while a security policy is in effect.
$context = stream_context_create([
'http' => [
'follow_location' => 0,
'max_redirects' => 1,
],
]);
$content = @file_get_contents($params['file'], false, $context);
} else {
$content = @file_get_contents($params['file']);
}
$content = @file_get_contents($params['file']);
if ($content === false) {
throw new Exception("{fetch} cannot read resource '" . $params['file'] . "'");
}
+3 -8
View File
@@ -65,7 +65,7 @@ class HtmlImage extends Base {
break;
case 'link':
case 'href':
$prefix = '<a href="' . smarty_function_escape_special_chars($_val) . '">';
$prefix = '<a href="' . $_val . '">';
$suffix = '</a>';
break;
default:
@@ -143,12 +143,7 @@ class HtmlImage extends Base {
$width = round($width * $_resize);
$height = round($height * $_resize);
}
// $alt and the pass-through attributes ($extra) are already escaped above;
// escape the remaining value-context params at output time so untrusted
// values cannot break out of the attribute (CWE-79). The unescaped $file/
// $width/$height are still used for getimagesize()/DPI math above.
return $prefix . '<img src="' . smarty_function_escape_special_chars($path_prefix . $file) . '" alt="' . $alt
. '" width="' . smarty_function_escape_special_chars($width) . '" height="'
. smarty_function_escape_special_chars($height) . '"' . $extra . ' />' . $suffix;
return $prefix . '<img src="' . $path_prefix . $file . '" alt="' . $alt . '" width="' . $width . '" height="' .
$height . '"' . $extra . ' />' . $suffix;
}
}
+3 -7
View File
@@ -120,6 +120,9 @@ class HtmlSelectDate extends Base {
case 'day_value_format':
case 'month_format':
case 'month_value_format':
case 'day_size':
case 'month_size':
case 'year_size':
case 'all_extra':
case 'day_extra':
case 'month_extra':
@@ -137,13 +140,6 @@ class HtmlSelectDate extends Base {
case 'year_id':
$$_key = (string)$_value;
break;
case 'day_size':
case 'month_size':
case 'year_size':
// numeric HTML size attribute; cast to int (consistent with
// html_select_time) so it cannot break out of size="…" (CWE-79)
$$_key = (int)$_value;
break;
case 'display_days':
case 'display_months':
case 'display_years':
+1 -12
View File
@@ -54,19 +54,8 @@ class StreamPlugin extends RecompiledPlugin {
$filepath = str_replace(':', '://', $source->getFullResourceName());
}
// Validate the underlying stream wrapper against the security policy.
// When the built-in "stream" resource type is used (e.g.
// stream:php://filter/...), BasePlugin::load() matches the "stream"
// sysplugin before the stream_get_wrappers()/isTrustedStream() check,
// so the nested wrapper ("php" here) is never validated. Parse the
// wrapper scheme from the resolved path and check it explicitly so that
// e.g. Security::$streams = null blocks it before fopen() (CWE-22/-441).
$smarty = $source->getSmarty();
if (is_object($smarty->security_policy) && ($_pos = strpos($filepath, '://')) !== false) {
$smarty->security_policy->isTrustedStream(strtolower(substr($filepath, 0, $_pos)));
}
$t = '';
// the availability of the stream has already been checked in Smarty\Resource\Base::fetch()
$fp = fopen($filepath, 'r+');
if ($fp) {
while (!feof($fp) && ($current_line = fgets($fp)) !== false) {
+4 -30
View File
@@ -52,7 +52,7 @@ class Security {
/**
* This is an array of trusted static classes.
* If empty access to all static classes is allowed.
* To disable access to all static classes set $static_classes = null.
* If set to 'none' none is allowed.
*
* @var array
*/
@@ -206,11 +206,7 @@ class Security {
* @return boolean true if class is trusted
*/
public function isTrustedStaticClass($class_name, $compiler) {
// Only an array enables access: an empty array allows all classes, a
// populated array is an allowlist. Any other value (null, or the
// documented "none") denies all. Using is_array() rather than isset()
// also avoids a PHP 8 TypeError from passing a non-array to in_array().
if (is_array($this->static_classes)
if (isset($this->static_classes)
&& (empty($this->static_classes) || in_array($class_name, $this->static_classes))
) {
return true;
@@ -478,34 +474,12 @@ class Security {
* @throws \Smarty\Exception
*/
private function _checkDir($filepath, $dirs) {
// Resolve the canonical, symlink-free path of the requested file so that
// a symlink located inside a trusted directory cannot be abused to read
// a file outside of it (CWE-22 path traversal). Smarty::_realpath() only
// normalizes the path as a string and does not follow symlinks, so we
// fall back to it only when the file does not yet exist on disk (e.g.
// config/cache paths that are validated before being written).
$realpath = @realpath($filepath);
$resolved = $realpath !== false ? $realpath : $this->smarty->_realpath($filepath, true);
$directory = dirname($resolved) . DIRECTORY_SEPARATOR;
// Canonicalize the trusted directories the same way. This keeps
// legitimate symlinked deployment paths working (e.g. a Capistrano-style
// "current" release symlink, or macOS' /var -> /private/var): both the
// file and the trusted directories are compared after symlinks have been
// resolved.
$trusted = [];
foreach ($dirs as $dir => $unused) {
$trusted[$dir] = true;
if (($dirRealpath = @realpath($dir)) !== false) {
$trusted[rtrim($dirRealpath, '\\/') . DIRECTORY_SEPARATOR] = true;
}
}
$directory = dirname($this->smarty->_realpath($filepath, true)) . DIRECTORY_SEPARATOR;
$_directory = [];
if (!preg_match('#[\\\\/][.][.][\\\\/]#', $directory)) {
while (true) {
// test if the directory is trusted
if (isset($trusted[$directory])) {
if (isset($dirs[$directory])) {
return $_directory;
}
// abort if we've reached root
+1 -1
View File
@@ -54,7 +54,7 @@ class Smarty extends \Smarty\TemplateBase {
/**
* smarty version
*/
const SMARTY_VERSION = '5.8.4';
const SMARTY_VERSION = '5.8.0';
/**
* define caching modes
+2 -6
View File
@@ -260,11 +260,7 @@ class Template extends TemplateBase {
$tpl = $this->smarty->doCreateTemplate($template_name, $cache_id, $compile_id, $this, $caching, $cache_lifetime);
// Re-use the same Inheritance object only inside an active inheritance tree, i.e. when this
// (including) template already has one. A template outside any inheritance tree has no
// Inheritance object (null); sub-templates it {include}s must then start with their own, so an
// {include}d template that uses {block}/{extends} creates a fresh root via getInheritance().
$tpl->inheritance = $this->inheritance;
$tpl->inheritance = $this->getInheritance(); // re-use the same Inheritance object inside the inheritance tree
if ($scope) {
$tpl->defaultScope = $scope;
@@ -535,7 +531,7 @@ class Template extends TemplateBase {
*/
public function getRightDelimiter()
{
return $this->right_delimiter ?? $this->getSmarty()->getRightDelimiter();
return $this->right_delimiter ?? $this->getSmarty()->getRightDelimiter();;
}
/**
@@ -17,7 +17,11 @@ class UserliteralTest extends PHPUnit_Smarty
{
public function setUp(): void
{
$this->setUpSmarty(__DIR__);
if (!property_exists('Smarty', 'literals')) {
$this->markTestSkipped('user literal support');
} else {
$this->setUpSmarty(__DIR__);
}
}
@@ -65,6 +65,15 @@ class EvalResourceTest extends PHPUnit_Smarty
$this->assertEquals('', $this->smarty->fetch($tpl));
}
/**
* test usesCompiler
*/
public function testUsesCompiler()
{
$tpl = $this->smarty->createTemplate('eval:hello world');
$this->markTestIncomplete();
}
/**
* test isEvaluated
*/
@@ -86,6 +86,12 @@ class FileResourceTest extends PHPUnit_Smarty
$this->assertEquals('hello world', $tpl->getSource()->getContent());
}
public function testUsesCompiler()
{
$tpl = $this->smarty->createTemplate('helloworld.tpl');
$this->markTestIncomplete();
}
public function testIsEvaluated()
{
$tpl = $this->smarty->createTemplate('helloworld.tpl');
@@ -60,6 +60,15 @@ class StreamResourceTest extends PHPUnit_Smarty
$this->assertEquals('hello world {$foo}', $tpl->getSource()->getContent());
}
/**
* test usesCompiler
*/
public function testUsesCompiler()
{
$tpl = $this->smarty->createTemplate('global:mytest');
$this->markTestIncomplete();
}
/**
* test isEvaluated
*/
@@ -73,6 +73,15 @@ class StringResourceTest extends PHPUnit_Smarty
$this->assertEquals('hello world{$foo}', $tpl->getSource()->getContent());
}
/**
* test usesCompiler
*/
public function testUsesCompiler()
{
$tpl = $this->smarty->createTemplate('string:hello world');
$this->markTestIncomplete();
}
/**
* test isEvaluated
*/
@@ -243,39 +243,6 @@ class SecurityTest extends PHPUnit_Smarty
$this->smarty->fetch('string:{$smarty.template_object::square(5)}');
}
/**
* The default (empty array) allows access to all static classes. Documents
* the backwards-compatible behaviour.
*/
public function testStaticClassAllowedByDefault()
{
$this->smarty->security_policy->static_classes = array();
$this->assertEquals('25', $this->smarty->fetch('string:{mysecuritystaticclass::square(5)}'));
}
/**
* Setting static_classes to null disables access to all static classes.
*/
public function testStaticClassDeniedWhenNull()
{
$this->expectException(\Smarty\Exception::class);
$this->expectExceptionMessage("access to static class 'mysecuritystaticclass' not allowed by security setting");
$this->smarty->security_policy->static_classes = null;
$this->smarty->fetch('string:{mysecuritystaticclass::square(5)}');
}
/**
* Regression: a non-array value such as the string 'none' must deny access
* cleanly instead of raising a PHP 8 TypeError from in_array().
*/
public function testStaticClassDeniedWhenNonArray()
{
$this->expectException(\Smarty\Exception::class);
$this->expectExceptionMessage("access to static class 'mysecuritystaticclass' not allowed by security setting");
$this->smarty->security_policy->static_classes = 'none';
$this->smarty->fetch('string:{mysecuritystaticclass::square(5)}');
}
public function testChangedTrustedDirectory()
{
$this->smarty->security_policy->secure_dir = array(
@@ -289,88 +256,6 @@ class SecurityTest extends PHPUnit_Smarty
);
$this->assertEquals("templates_3", $this->smarty->fetch('string:{include file="templates_3/dirname.tpl"}'));
}
/**
* A symlink located inside a trusted secure_dir must not be usable to read
* a file outside of it (CWE-22 path traversal via symlink).
*/
public function testSymlinkEscapeFromSecureDirIsRejected()
{
[$secureDir, $outsideFile] = $this->createSymlinkFixture('secret-outside-content');
$link = $secureDir . DIRECTORY_SEPARATOR . 'finance_doc';
if (!@symlink($outsideFile, $link)) {
$this->markTestSkipped('Unable to create symlinks on this platform');
}
$this->smarty->security_policy->secure_dir = array($secureDir . DIRECTORY_SEPARATOR);
$this->expectException(\Smarty\Exception::class);
$this->expectExceptionMessage('not trusted file path');
// Use forward slashes: backslashes in a double-quoted template string are
// interpreted as escape sequences (\f, \r, ...), which would corrupt a
// Windows path. Forward slashes work on every platform.
$this->smarty->fetch('string:{include file="' . str_replace('\\', '/', $link) . '"}');
}
/**
* A symlink that stays inside the trusted secure_dir must keep working, so
* legitimate (e.g. deployment) symlinks are not broken by the fix above.
*/
public function testSymlinkWithinSecureDirIsAllowed()
{
[$secureDir] = $this->createSymlinkFixture('secret-outside-content');
$target = $secureDir . DIRECTORY_SEPARATOR . 'real.tpl';
file_put_contents($target, 'inside-content');
$link = $secureDir . DIRECTORY_SEPARATOR . 'linked.tpl';
if (!@symlink($target, $link)) {
$this->markTestSkipped('Unable to create symlinks on this platform');
}
$this->smarty->security_policy->secure_dir = array($secureDir . DIRECTORY_SEPARATOR);
// Forward slashes so backslashes in a Windows path are not mistaken for
// escape sequences inside the double-quoted template string.
$this->assertEquals('inside-content', $this->smarty->fetch('string:{include file="' . str_replace('\\', '/', $link) . '"}'));
}
/**
* Builds a temporary directory tree for the symlink tests: a (canonicalized)
* secure directory plus a file located outside of it. The tree is removed in
* tearDown(). Returns [secureDir, outsideFile].
*/
private function createSymlinkFixture(string $outsideContent): array
{
$base = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'smarty_symlink_' . getmypid() . '_' . uniqid();
$secureDir = $base . DIRECTORY_SEPARATOR . 'secure';
mkdir($secureDir, 0777, true);
$outsideFile = $base . DIRECTORY_SEPARATOR . 'outside.txt';
file_put_contents($outsideFile, $outsideContent);
// Canonicalize so secure_dir is symlink-free (sys_get_temp_dir() itself
// may sit under a symlink, e.g. /var -> /private/var on macOS).
$this->symlinkFixtureDir = realpath($base);
return array(realpath($secureDir), realpath($outsideFile));
}
protected function tearDown(): void
{
if (!empty($this->symlinkFixtureDir) && is_dir($this->symlinkFixtureDir)) {
$it = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($this->symlinkFixtureDir, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($it as $entry) {
($entry->isDir() && !$entry->isLink()) ? rmdir($entry->getPathname()) : unlink($entry->getPathname());
}
rmdir($this->symlinkFixtureDir);
$this->symlinkFixtureDir = null;
}
parent::tearDown();
}
/** @var string|null temp dir created by createSymlinkFixture(), removed in tearDown */
private $symlinkFixtureDir = null;
/**
* test template file exits
*
@@ -1,139 +0,0 @@
<?php
/**
* Smarty PHPunit tests for stream-wrapper security
*
* @package PHPunit
*/
/**
* Regression tests ensuring the built-in "stream" resource type cannot be used
* to bypass the stream-wrapper restrictions enforced by Smarty Security.
*
* @runTestsInSeparateProcess
* @preserveGlobalState disabled
* @backupStaticAttributes enabled
*/
class StreamWrapperSecurityTest extends PHPUnit_Smarty
{
private $secretFile;
public function setUp(): void
{
$this->setUpSmarty(__DIR__);
$this->secretFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR
. 'smarty_stream_secret_' . getmypid() . '_' . uniqid() . '.txt';
file_put_contents($this->secretFile, 'STREAM-WRAPPER-SECRET');
$this->smarty->setForceCompile(true);
$this->smarty->enableSecurity();
}
public function tearDown(): void
{
if ($this->secretFile && file_exists($this->secretFile)) {
unlink($this->secretFile);
}
parent::tearDown();
}
private function phpFilterUri()
{
return 'php://filter/read=convert.base64-encode/resource=' . $this->secretFile;
}
/**
* Sanity: a direct php:// stream is rejected when all streams are disabled.
*/
public function testDirectPhpStreamIsBlocked()
{
$this->smarty->security_policy->streams = null;
$this->expectException(\Smarty\Exception::class);
$this->expectExceptionMessage("stream 'php' not allowed by security setting");
$this->smarty->fetch('string:{include file="' . $this->phpFilterUri() . '"}');
}
/**
* The built-in "stream" resource type must not let a nested php:// wrapper
* escape the same restriction (CWE-22 / wrapper bypass).
*/
public function testStreamResourceCannotBypassDisabledStreams()
{
$this->smarty->security_policy->streams = null;
$this->expectException(\Smarty\Exception::class);
$this->expectExceptionMessage("stream 'php' not allowed by security setting");
$this->smarty->fetch('string:{include file="stream:' . $this->phpFilterUri() . '"}');
}
/**
* Even when some streams are allowed, a nested wrapper that is not on the
* allowlist must still be rejected through the "stream" resource type.
*/
public function testStreamResourceRejectsWrapperNotOnAllowlist()
{
$this->smarty->security_policy->streams = array('file');
$this->expectException(\Smarty\Exception::class);
$this->expectExceptionMessage("stream 'php' not allowed by security setting");
$this->smarty->fetch('string:{include file="stream:' . $this->phpFilterUri() . '"}');
}
/**
* A wrapper explicitly allowed by the policy must keep working through the
* "stream" resource type (no backwards-compatibility break).
*/
public function testStreamResourceAllowsWhitelistedWrapper()
{
stream_wrapper_register('smartyteststream', 'StreamSecurityTestWrapper');
try {
$this->smarty->security_policy->streams = array('smartyteststream');
$this->smarty->assign('name', 'World');
$result = $this->smarty->fetch('string:{include file="stream:smartyteststream://x"}');
$this->assertEquals('hello World', $result);
} finally {
stream_wrapper_unregister('smartyteststream');
}
}
}
/**
* Minimal read-only stream wrapper returning a fixed template body, used by the
* allowlist (positive) test above.
*/
#[AllowDynamicProperties]
class StreamSecurityTestWrapper
{
public $context;
private $pos = 0;
private $data = 'hello {$name}';
public function stream_open($path, $mode, $options, &$opened_path)
{
$this->pos = 0;
return true;
}
public function stream_read($count)
{
$ret = substr($this->data, $this->pos, $count);
$this->pos += strlen($ret);
return $ret;
}
public function stream_eof()
{
return $this->pos >= strlen($this->data);
}
public function stream_stat()
{
return array();
}
public function url_stat($path, $flags)
{
return array();
}
public function stream_seek($offset, $whence)
{
return false;
}
}
@@ -1,52 +1,50 @@
<?php
// first class callables where introduced in PHP 8.1
if (PHP_VERSION_ID >= 80100) {
/**
* class for register modifier with (first class) callables tests
*
* @runTestsInSeparateProcess
* @preserveGlobalState disabled
* @backupStaticAttributes enabled
*/
class RegisterModifierFirstClassCallablesTest extends PHPUnit_Smarty
{
public function setUp(): void
/**
* class for register modifier with (first class) callables tests
*
* @runTestsInSeparateProcess
* @preserveGlobalState disabled
* @backupStaticAttributes enabled
*/
class RegisterModifierFirstClassCallablesTest extends PHPUnit_Smarty
{
// First-class callable syntax (Closure::fromCallable shorthand) requires PHP 8.1+
if (PHP_VERSION_ID < 80100) {
$this->markTestSkipped('First-class callables require PHP >= 8.1');
public function setUp(): void
{
$this->setUpSmarty(__DIR__);
}
$this->setUpSmarty(__DIR__);
public function testRegisterFirstClassCallable()
{
$this->smarty->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'testmodifier', eval('return strrev(...);'));
$this->assertEquals('mosredna', $this->smarty->fetch('string:{"andersom"|testmodifier}'));
}
public function testRegisterFirstClassCallableSameName()
{
$this->smarty->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'mymodifier', eval('return strrev(...);'));
$this->assertEquals('mosredna', $this->smarty->fetch('string:{"andersom"|mymodifier}'));
}
public function testRegisterFirstClassCallableAsFunc()
{
$this->smarty->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'kprint_r_out', eval('return strrev(...);'));
$this->smarty->assign('myVar', 'andersom');
$this->assertEquals('mosredna', $this->smarty->fetch('string:{kprint_r_out($myVar)}'));
}
public function testRegisterFirstClassCallableSameNameAsPhpFunc()
{
$this->smarty->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'mymodifierfcc', eval('return strrev(...);'));
$this->assertEquals('mosredna', $this->smarty->fetch('string:{mymodifierfcc("andersom")}'));
}
}
public function testRegisterFirstClassCallable()
{
$this->smarty->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'testmodifier', eval('return strrev(...);'));
$this->assertEquals('mosredna', $this->smarty->fetch('string:{"andersom"|testmodifier}'));
}
public function testRegisterFirstClassCallableSameName()
{
$this->smarty->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'mymodifier', eval('return strrev(...);'));
$this->assertEquals('mosredna', $this->smarty->fetch('string:{"andersom"|mymodifier}'));
}
public function testRegisterFirstClassCallableAsFunc()
{
$this->smarty->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'kprint_r_out', eval('return strrev(...);'));
$this->smarty->assign('myVar', 'andersom');
$this->assertEquals('mosredna', $this->smarty->fetch('string:{kprint_r_out($myVar)}'));
}
public function testRegisterFirstClassCallableSameNameAsPhpFunc()
{
$this->smarty->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'mymodifierfcc', eval('return strrev(...);'));
$this->assertEquals('mosredna', $this->smarty->fetch('string:{mymodifierfcc("andersom")}'));
}
}
function mymodifierfcc($a, $b, $c)
{
return "$a function $b $c";
@@ -191,45 +191,4 @@ class CompileForTest extends PHPUnit_Smarty
array("{for \$x=-1;\$x>=0;\$x--}{\$x}{forelse}{\$buh}{/for}", "buh", 'T14', $i++),
);
}
/**
* Test {for} inside an inheritance (extends) template.
*
* The {for} tag pokes loop bookkeeping (step, total, value, ...) directly
* onto its loop Variable. In an extended template the child block renders
* with SCOPE_PARENT, so the loop variable is assigned to the parent and was
* not resolvable locally, causing 'assign property on null'.
*
* @see https://github.com/smarty-php/smarty/issues/1036
*
* @dataProvider dataForInheritance
*/
public function testForInheritance($code, $result, $testName, $caching)
{
$this->smarty->caching = $caching;
$tpl = $this->smarty->createTemplate($code);
$this->assertEquals($result, $this->smarty->fetch($tpl), "test - {$testName}");
}
public function dataForInheritance()
{
$parent = 'extends:string:{block name="content"}{/block}';
$cases = array(
array('to', '{for $i=0 to 3}{$i}{/for}', '0123'),
array('step', '{for $i=0 to 3 step 2}{$i}{/for}', '02'),
array('max', '{for $i=0 to 30 max=3}{$i}{/for}', '012'),
array('nested', '{for $i=0 to 1}{for $y=0 to 3}{$y}{/for}{/for}', '01230123'),
array('legacy', '{for $i=0; $i<4; $i++}{$i}{/for}', '0123'),
);
$data = array();
foreach (array(false, true) as $caching) {
foreach ($cases as $case) {
$code = $parent . '|string:{block name="content"}' . $case[1] . '{/block}';
$name = $case[0] . ($caching ? ' (caching)' : '');
$data[] = array($code, $case[2], $name, $caching);
}
}
return $data;
}
}
@@ -64,93 +64,4 @@ class PluginFunctionFetchTest extends PHPUnit_Smarty
$this->smarty->fetch('string:{fetch file="/templates/../etc/passwd"}');
}
/**
* When a security policy is in effect, {fetch} of a remote resource must not
* follow redirects, otherwise an open redirect on a trusted host could be
* used to bypass trusted_uri and reach an internal target (SSRF, CWE-918).
*/
public function testFetchRemoteDisablesRedirectsUnderSecurity()
{
FetchContextCaptureStreamWrapper::$capturedOptions = null;
stream_wrapper_register('ssrftest', FetchContextCaptureStreamWrapper::class);
try {
$this->smarty->enableSecurity();
$this->smarty->security_policy->trusted_uri[] = '/^ssrftest:\/\/allowed$/';
$result = $this->smarty->fetch('string:{fetch file="ssrftest://allowed/data"}');
$this->assertSame('BODY', $result);
$this->assertIsArray(FetchContextCaptureStreamWrapper::$capturedOptions);
$this->assertArrayHasKey('http', FetchContextCaptureStreamWrapper::$capturedOptions);
$this->assertSame(0, FetchContextCaptureStreamWrapper::$capturedOptions['http']['follow_location']);
$this->assertLessThanOrEqual(1, FetchContextCaptureStreamWrapper::$capturedOptions['http']['max_redirects']);
} finally {
stream_wrapper_unregister('ssrftest');
}
}
/**
* Without a security policy there is no trusted_uri to bypass, so the
* redirect-disabling stream context is not applied (backwards compatible).
*/
public function testFetchRemoteKeepsDefaultBehaviorWithoutSecurity()
{
FetchContextCaptureStreamWrapper::$capturedOptions = null;
stream_wrapper_register('ssrftest', FetchContextCaptureStreamWrapper::class);
try {
$result = $this->smarty->fetch('string:{fetch file="ssrftest://allowed/data"}');
$this->assertSame('BODY', $result);
$this->assertSame([], FetchContextCaptureStreamWrapper::$capturedOptions);
} finally {
stream_wrapper_unregister('ssrftest');
}
}
}
/**
* Minimal custom stream wrapper used by the fetch SSRF tests: it records the
* stream context options that {fetch} passes to file_get_contents() and returns
* a fixed body so the call succeeds without touching the network.
*/
class FetchContextCaptureStreamWrapper
{
/** @var resource|null populated by PHP when a context is passed */
public $context;
/** @var array|null options captured from the context on the last open */
public static $capturedOptions = null;
private $read = false;
public function stream_open($path, $mode, $options, &$opened_path)
{
self::$capturedOptions = isset($this->context) ? stream_context_get_options($this->context) : [];
return true;
}
public function stream_read($count)
{
if ($this->read) {
return '';
}
$this->read = true;
return 'BODY';
}
public function stream_eof()
{
return $this->read;
}
public function stream_stat()
{
return [];
}
public function url_stat($path, $flags)
{
return [];
}
}
@@ -1,79 +0,0 @@
<?php
/**
* Smarty PHPunit tests of the {html_image} function plugin
*/
/**
* class for {html_image} tests
*/
class PluginFunctionHtmlImageTest extends PHPUnit_Smarty
{
public function setUp(): void
{
$this->setUpSmarty(__DIR__);
$this->smarty->setErrorReporting(E_ALL & ~E_DEPRECATED);
}
public function testInit()
{
$this->assertTrue($this->smarty instanceof \Smarty\Smarty);
}
/**
* Passing both width and height skips the getimagesize() lookup, so no real
* image file is needed to render the tag.
*/
private function render($params)
{
$tpl = $this->smarty->createTemplate('eval:{html_image file=$file width=$width height=$height href=$href path_prefix=$path_prefix}');
$tpl->assign($params + [
'file' => 'pic.jpg',
'width' => 44,
'height' => 68,
'href' => '',
'path_prefix' => '',
]);
return $tpl->fetch();
}
public function testHrefIsEscaped()
{
$result = $this->render(['href' => '"><script>alert(1)</script>']);
$this->assertStringNotContainsString('<script>', $result);
$this->assertStringContainsString('&lt;script&gt;', $result);
}
public function testWidthIsEscaped()
{
$result = $this->render(['width' => '44" onload="alert(1)']);
$this->assertStringNotContainsString('onload="', $result);
$this->assertStringContainsString('&quot;', $result);
}
public function testHeightIsEscaped()
{
$result = $this->render(['height' => '68" onmouseover="alert(1)']);
$this->assertStringNotContainsString('onmouseover="', $result);
}
public function testFileAndPathPrefixAreEscaped()
{
$result = $this->render(['file' => 'pic.jpg"><script>alert(1)</script>', 'path_prefix' => '"><b>']);
$this->assertStringNotContainsString('<script>', $result);
$this->assertStringNotContainsString('<b>', $result);
}
/**
* Benign values must be unchanged (no breakage, no double-encoding of an
* ampersand already present in a URL).
*/
public function testBenignValuesAreUnchanged()
{
$result = $this->render(['width' => 44, 'height' => 68, 'href' => 'detail.php?id=1&page=2']);
$this->assertStringContainsString('width="44"', $result);
$this->assertStringContainsString('height="68"', $result);
$this->assertStringContainsString('src="pic.jpg"', $result);
$this->assertStringContainsString('href="detail.php?id=1&amp;page=2"', $result);
$this->assertStringNotContainsString('&amp;amp;', $result);
}
}
@@ -630,25 +630,4 @@ class PluginFunctionHtmlSelectDateTest extends PHPUnit_Smarty
$tpl->assign('date_array', $date_array);
$this->assertEquals($result, $tpl->fetch());
}
/**
* year_size/month_size/day_size are numeric HTML size attributes and must be
* cast to int (like html_select_time), so a value cannot break out of size="…".
*/
public function testSizeAttributesCannotBreakOut()
{
$tpl = $this->smarty->createTemplate('eval:{html_select_date time=$time year_size=$size}');
$tpl->assign('time', mktime(0, 0, 0, 1, 1, 2010));
$tpl->assign('size', '2"><script>alert(1)</script>');
$result = $tpl->fetch();
$this->assertStringNotContainsString('<script>', $result);
$this->assertStringContainsString('size="2"', $result);
}
public function testBenignSizeAttributeIsKept()
{
$tpl = $this->smarty->createTemplate('eval:{html_select_date time=$time year_size=3}');
$tpl->assign('time', mktime(0, 0, 0, 1, 1, 2010));
$this->assertStringContainsString('size="3"', $tpl->fetch());
}
}
@@ -1,33 +0,0 @@
<?php
/**
* Smarty PHPunit test reproducing issue #1189.
*
* When a parent template is {include}d, then a child template that {extends}
* the parent overrides a {block}, a subsequent {include} of the parent in the
* same render must still show the parent's block content.
*
* The block override from the extending child must not leak into the later
* include of the parent template.
*
* @see https://github.com/smarty-php/smarty/issues/1189
*
* @preserveGlobalState disabled
*/
class IncludeExtendsBlockLeakIssue1189Test extends PHPUnit_Smarty
{
public function setUp(): void
{
$this->setUpSmarty(__DIR__);
}
/**
* Sequence: include parent -> include child(extends parent) -> include parent.
* Expected: PARENT CHILD PARENT
* Bug (#1189): PARENT CHILD CHILD
*/
public function testBlockOverrideDoesNotLeakIntoLaterParentInclude()
{
$result = $this->smarty->fetch('top.tpl');
$this->assertSame('PARENT CHILD PARENT', preg_replace('/\s+/', ' ', trim($result)));
}
}
@@ -1,2 +0,0 @@
{extends file="parent.tpl"}
{block name=message}CHILD{/block}
@@ -1 +0,0 @@
{block name=message}PARENT{/block}
@@ -1 +0,0 @@
{include file="parent.tpl"} {include file="child.tpl"} {include file="parent.tpl"}