Compare commits

..

48 Commits

Author SHA1 Message Date
colemanw 1f9eef51c7 Fix double semicolon in getRightDelimiter method (#1202) 2026-07-04 13:37:14 +02:00
Simon Wisselink 94a27cbbc7 Merge branch 'release/5.8.4' 2026-06-29 12:46:32 +02:00
Simon Wisselink badc5ef3a0 version bump 2026-06-29 12:46:30 +02:00
Simon Wisselink 2ae0f9a65f Fix TypeError for non-array static_classes in Security policy (#1198) 2026-06-29 12:45:58 +02:00
Simon Wisselink b668745acf drop unused version attribute from docker-compose.yml 2026-06-29 12:40:41 +02:00
Simon Wisselink 3c9f77a2e0 Security: validate nested stream wrapper in stream: resource (CWE-22) (#1195)
The built-in stream: resource type let a template bypass Security stream
restrictions. BasePlugin::load() matches the 'stream' sysplugin before the
stream_get_wrappers()/isTrustedStream() check, so a resource such as
stream:php://filter/read=convert.base64-encode/resource=/path was opened by
StreamPlugin::getContent() via fopen() on the nested php:// wrapper without
ever validating it. This bypassed Security::$streams (including
Security::$streams = null) and allowed reading arbitrary local files.

Parse the wrapper scheme from the resolved path in StreamPlugin::getContent()
and validate it with Security::isTrustedStream() before fopen(), giving the
stream: resource the same check the direct wrapper path already receives.

Adds regression tests covering the disabled-streams bypass, the
not-on-allowlist case, and a positive test that an explicitly allowed wrapper
still works.
2026-06-29 11:47:32 +02:00
Simon Wisselink 042dff64a9 Merge branch 'release/5.8.3' 2026-06-29 00:15:09 +02:00
Simon Wisselink 1830aa7a12 version bump 2026-06-29 00:15:07 +02:00
Simon Wisselink b83ffdd2d6 requirements for building docs, switched test-runner from mutagen to basic docker compose 2026-06-29 00:14:40 +02:00
Simon Wisselink ac27e1e5b6 fixed a regression from #1189 where a child template's block override no longer applied to a template {include}d by the parent
Fixes #1192
2026-06-29 00:12:45 +02:00
Simon Wisselink 17fae11a38 update documentation for building and previewing with mkdocs, fix unit tests for windows 2026-06-24 10:33:14 +02:00
Simon Wisselink 06594a1149 Merge branch 'release/5.8.2' 2026-06-24 10:04:41 +02:00
Simon Wisselink 75a738ad44 version bump 2026-06-24 10:04:39 +02:00
Simon Wisselink 11e69eca68 Security: escape value-context attributes in html_image/html_select_date (CWE-79)
{html_image} already escaped alt and pass-through attributes, but emitted
file, path_prefix, href/link, width and height raw, letting an untrusted
value break out of the generated tag. Escape these at output time; the
unescaped values are still used for getimagesize()/DPI math. Escaping uses
htmlspecialchars with double_encode=false, so existing entities and values
like "100%" are preserved (no BC break for legitimate values).

{html_select_date} treated day_size/month_size/year_size as strings and
emitted them raw into size="…"; cast them to int to match
{html_select_time} and close the breakout.

The remaining flagged parameters (mailto extra; html_table *_attr/
trailpad/caption/loop; html_radios/html_checkboxes separator;
html_select_* *_extra/field_separator and the unrecognised-attribute
pass-through) intentionally emit raw markup as documented, so escaping
them would break backwards compatibility. Add a security note to those
docs pages instead, telling authors to escape untrusted values themselves.

Adds tests for html_image escaping (incl. benign-value/no-double-encode
checks) and the html_select_date size cast.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 00:48:01 +02:00
Simon Wisselink 31e06fc087 Security: stop {fetch} from following redirects past trusted_uri (CWE-918)
{fetch} validates the requested URL with Security::isTrustedUri(), but
for non-http schemes (e.g. https) it reads the resource via
file_get_contents(), which follows redirects by default. An open redirect
on an otherwise trusted host could therefore be used to reach a
non-trusted, internal target, bypassing the trusted_uri policy (SSRF).

When a security policy is active, pass a stream context that disables
redirect-following (follow_location => 0, max_redirects => 1) to
file_get_contents() for remote resources. Behavior is unchanged when no
security policy is set, since there is no trusted_uri to bypass.

Adds a regression test using a custom stream wrapper that captures the
context {fetch} passes to file_get_contents, plus a backwards-compat test
for the no-security-policy case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 00:21:12 +02:00
Simon Wisselink 99c048ce7a Security: prevent symlink path traversal out of secure_dir (CWE-22)
Security::_checkDir() validated file access using Smarty::_realpath(),
which only normalizes paths as strings and never follows symlinks. A
symlink placed inside a trusted secure_dir/template directory therefore
passed the trust check while file_get_contents() followed it to an
arbitrary file (e.g. /etc/passwd), affecting {include} and {fetch} of
local files.

Resolve the requested file with native realpath() and re-validate the
canonical, symlink-free path against the trusted directories. The trusted
directories are canonicalized the same way so legitimate symlinked
deployment paths (e.g. a Capistrano "current" release symlink, or macOS'
/var -> /private/var) keep working. Falls back to string normalization
only when the file does not yet exist on disk.

Adds regression tests covering both the rejected escape and an allowed
in-sandbox symlink, and documents the changelog convention in AGENTS.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 00:15:19 +02:00
Antonio Norman 1c9b2ce1d3 fix for Error: Attempt to assign property "step" on null closes issue #1036 (#1071)
* fix for Error: Attempt to assign property "step" on null in extended templates + added tests closes issue #1036
2026-06-23 23:50:43 +02:00
Simon Wisselink 29a77ded2f Merge branch 'release/5.8.1' 2026-06-23 23:04:23 +02:00
Simon Wisselink 6d06c5e61f version bump 2026-06-23 23:04:21 +02:00
Simon Wisselink ba745f5b91 Added changelog for the fix for issue #1189 2026-06-23 23:03:58 +02:00
Simon Wisselink 0775fa84d1 Agents instructions 2026-06-23 22:52:51 +02:00
Simon Wisselink 56ab75df2c Bugfix/issue 1189 inheritance state leak (#1190)
* Reproduce block override leakage in template inheritance
* Fixes #1189
2026-06-23 22:50:39 +02:00
Masatoshi Ogiwara 383e20e6b4 fix: return type (#1180) 2026-05-16 00:34:29 +02:00
Simon Wisselink c139883770 update todos 2026-05-03 22:19:59 +02:00
Simon Wisselink 6e648ed809 Remove incomplete test cases for usesCompiler across multiple test files 2026-05-03 22:19:50 +02:00
Simon Wisselink 3577fc7091 Re-activate unit tests for user literals. 2026-04-13 22:31:06 +02:00
Simon Wisselink ff2ef3b0cb Redirect test temp dirs to system temp directory
* 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.

* 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.

* 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

* 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.

* cleanup of unused template files, non-shared files stored in __shared folder, no longer required calls to add template folders et cetera

* fixed the unit tests

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* remove useless resetting of static properties in tearDownAfterClass

* changed an incorrect doc and formatted some code.

* add changelog

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-13 21:36:33 +02:00
Simon Wisselink aa2dcd82cf Added AGENTS.md for improved vibe coding experience 2026-04-10 22:57:32 +02:00
Simon Wisselink 78d259d3b9 Merge branch 'release/5.8.0' 2026-02-15 15:27:15 +01:00
Simon Wisselink 4ed569c674 version bump 2026-02-15 15:27:13 +01:00
Simon Wisselink a46478d0cb changelogs 2026-02-15 15:26:43 +01:00
Simon Wisselink 5487e31c4b Add support for Backed Enums (#1171)
* Add support for Backed Enums
Fixes #1012

Also added docs (and docs for matches operator)
2026-02-15 15:23:55 +01:00
Simon Wisselink 12ce28e265 Regex matches operator (#1169)
* Regex matches operator support
2026-02-15 14:44:48 +01:00
Simon Wisselink 139797a165 Support for Laravel Collections style object chaining (#1168)
* Support for Laravel Collections style object chaining for objects return from function calls implemented as modifiers
Fixes #1151

* explain publishing docs
2026-02-11 00:02:52 +01:00
hirosan 6709d000cd Fix static analysis warnings for isDot() and remove deprecated APC support (#1164)
* Fix static analysis warnings for isDot()
* Remove deprecated APC support
* Remove redundant isDot() check and fix static analysis warnings
2026-01-08 11:21:57 +01:00
Simon Wisselink aa6edc3c0b Document missing inline implementation. Fixed #1152 (#1156) 2025-12-21 22:58:11 +01:00
Simon Wisselink 73da7e90f3 Merge branch 'release/5.7.0' 2025-11-19 22:36:38 +01:00
Simon Wisselink c0d7a36124 version bump 2025-11-19 22:36:36 +01:00
Simon Wisselink 8cc9a75964 Php8.5 support (#1138)
* PHP 8.5 support (using RC docker image for php 8.5 unit tests)
2025-11-19 22:33:49 +01:00
Wim Wisselink 21d7fbb67e Non-canonical cast (boolean) fix (#1145) 2025-10-17 14:45:38 +02:00
Simon Wisselink 1fc41e385d Merge branch 'release/5.6.0' 2025-10-03 23:22:25 +02:00
Simon Wisselink fe325daec0 version bump 2025-10-03 23:22:23 +02:00
Simon Wisselink 3f0f308a7b added changelog 2025-10-03 23:19:22 +02:00
pharixces b390e50974 Add support for shorttags in functions (#1142)
* Add support for shorttags in functions

Co-authored-by: Anne Zijlstra <a.zijlstra@iwink.nl>
Co-authored-by: Simon Wisselink <s.wisselink@iwink.nl>
2025-10-03 23:17:55 +02:00
Simon Wisselink 0e46ae3add Merge branch 'release/5.5.2' 2025-08-26 10:38:06 +02:00
Simon Wisselink 4d793ee04c version bump 2025-08-26 10:38:05 +02:00
Simon Wisselink 7677b84058 Fixed escaping of array/object keys in debug_print_var 2025-08-26 10:32:02 +02:00
Simon Wisselink e7457d78cb Add iWink and Temma logos 2025-05-27 22:39:34 +02:00
411 changed files with 3557 additions and 2671 deletions
+1
View File
@@ -34,6 +34,7 @@ jobs:
- "8.2"
- "8.3"
- "8.4"
- "8.5"
compiler:
- default
+2
View File
@@ -7,3 +7,5 @@ phpunit*
.phpunit.result.cache
vendor/*
composer.lock
+87
View File
@@ -0,0 +1,87 @@
# 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
# Install dependencies
composer install
# Regenerate lexers and parsers (required before tests; CI runs this)
make -B
# Run all tests
php ./vendor/phpunit/phpunit/phpunit
# Run a single test file
php ./vendor/phpunit/phpunit/phpunit tests/UnitTests/TemplateSource/TagTests/If/CompileIfTest.php
# Run tests matching a PHPUnit group
php ./vendor/phpunit/phpunit/phpunit --group 20221124
# Run tests excluding slow group
php ./vendor/phpunit/phpunit/phpunit --exclude-group slow
```
There is no linter or static analysis configured. No typecheck step.
## Generated code — do not hand-edit
Four files are generated from grammar/lexer definitions via `make`:
| Source (edit this) | Generated (do not edit) |
|---|---|
| `src/Lexer/TemplateLexer.plex` | `src/Lexer/TemplateLexer.php` |
| `src/Lexer/ConfigfileLexer.plex` | `src/Lexer/ConfigfileLexer.php` |
| `src/Parser/TemplateParser.y` | `src/Parser/TemplateParser.php` |
| `src/Parser/ConfigfileParser.y` | `src/Parser/ConfigfileParser.php` |
After editing a `.plex` or `.y` file, run `make -B` to regenerate. The generators require the `smarty/smarty-lexer` dev dependency.
## Architecture
- `src/Smarty.php` — main class, extends `TemplateBase`. Version constant: `Smarty::SMARTY_VERSION`.
- `src/Compile/`, `src/Compiler/` — template compilation pipeline.
- `src/Lexer/`, `src/Parser/` — lexer/parser (generated, see above).
- `src/Extension/` — extension system (`ExtensionInterface`, `CoreExtension`, `DefaultExtension`, `BCPluginsAdapter`).
- `src/Runtime/` — runtime helpers (foreach, capture, inheritance, tpl functions).
- `src/Resource/`, `src/Cacheresource/` — template resource and cache resource handlers.
- `src/BlockHandler/`, `src/FunctionHandler/`, `src/Filter/` — built-in tags, functions, and filters.
- `libs/Smarty.class.php` — non-Composer autoload stub. Points to `src/`. Not the main source.
- `src/functions.php` — global helper functions, always loaded via Composer `files` autoload.
## Tests
- Framework: PHPUnit 7.5/8.5 (bootstrap: `tests/Bootstrap.php`).
- 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).
## CI
GitHub Actions (`.github/workflows/ci.yml`): matrix of PHP 7.28.5 on ubuntu + windows. Steps: `composer install``make -B``phpunit`. No deploy step.
## 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`.
## 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).
+42
View File
@@ -6,6 +6,48 @@ 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)
- Update documentation to clarify that include inline is currently not implemented in Smarty v5 [#1152](https://github.com/smarty-php/smarty/issues/1152)
- Support for Laravel Collections style object chaining for objects return from function calls implemented as modifiers [#1151](https://github.com/smarty-php/smarty/issues/1151)
## [5.7.0] - 2025-11-19
- PHP 8.5 support
## [5.6.0] - 2025-10-03
- Added support for shorttags in functions [#1005](https://github.com/smarty-php/smarty/issues/1005)
## [5.5.2] - 2025-08-26
- Fixed escaping of array/object keys in debug_print_var
## [5.5.1] - 2025-05-19
- Fix missing support for loading modifiercompilers from plugin dir in BCPluginsAdapter [#1132](https://github.com/smarty-php/smarty/pull/1132)
+7
View File
@@ -100,6 +100,11 @@ 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
@@ -115,5 +120,7 @@ If you are a maintainer, you can publish the document using [mike](https://githu
mike deploy 5.x
```
Then, push the `gh-pages` branch.
## Attribution
This guide is based on the **contributing.md**. [Make your own](https://contributing.md/)!
+2 -1
View File
@@ -7,7 +7,7 @@ Smarty is a template engine for PHP, facilitating the separation of presentation
Read the [documentation](https://smarty-php.github.io/smarty/) to find out how to use it.
## Requirements
Smarty v5 can be run with PHP 7.2 to PHP 8.4.
Smarty v5 can be run with PHP 7.2 to PHP 8.5.
## Installation
Smarty versions 3.1.11 or later can be installed with [Composer](https://getcomposer.org/).
@@ -23,5 +23,6 @@ More in the [Getting Started](./docs/getting-started.md) section of the docs.
Smarty is sponsored by:
- Marc Laporte [@marclaporte](https://github.com/marclaporte)
- [Temma](https://github.com/Digicreon/Temma), the MVC framework based on Smarty
Thank you!
+4 -5
View File
@@ -11,10 +11,10 @@
## include inline
- Re-introduce merge_compiled_includes and the {include inline} attribute?
## Output buffering
- Fix ob_ output buffering commands being scattered around the codebase
## 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.
## Review public static vars
## Review public static vars (major)
- such as _CHARSET and _IS_WINDOWS
## Block / inheritance
@@ -24,9 +24,8 @@
## Plugin system
- fix template security checks in one place in compiler
## Beatify output
## Beatify output (major)
- 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
@@ -1,4 +1,3 @@
version: "2"
services:
base:
build:
@@ -50,6 +50,120 @@ separated from surrounding elements by spaces. Note that items listed in
| is \[not\] odd by | | $a is not odd by $b | \[not\] an odd grouping | ($a / $b) % 2 != 0 |
| is in | | $a is in $b | exists in array | in_array($a, $b) |
| is \[not\] in | | $a is not in $b | does not exist in array | !in_array($a, $b) |
| matches | | $a matches $b | regex pattern match | preg_match($b, $a) |
## Regex Matching Operator
The `matches` operator allows you to test if a string matches a regular expression pattern.
### Basic Usage
```smarty
{if "hello" matches "/^[a-z]+$/"}
String matches the pattern!
{/if}
{if $email matches "/^[^@]+@[^@]+\.[^@]+$/"}
Valid email format
{else}
Invalid email format
{/if}
```
### Using Variables
```smarty
{$pattern = '/^[a-zA-Z0-9]{8,}$/'}
{if $password matches $pattern}
Password meets requirements
{else}
Password must be at least 8 alphanumeric characters
{/if}
```
### Pattern Modifiers
The `matches` operator supports all standard PHP regex modifiers:
```smarty
{* Case insensitive matching *}
{if "HELLO" matches "/hello/i"}
Matches (case insensitive)
{/if}
{* Multiline mode *}
{if "line1\nline2" matches "/line2$/m"}
Matches in multiline mode
{/if}
{* Dot matches newlines *}
{if "hello\nworld" matches "/hello.world/s"}
Matches with dotall modifier
{/if}
```
### Complex Conditions
The `matches` operator can be combined with other operators:
```smarty
{if $username matches "/^[a-z]+$/" && $username|length > 3}
Valid username
{else}
Username must be lowercase letters and at least 4 characters
{/if}
{if $input matches "/^[0-9]+$/" || $input matches "/^[a-z]+$/"}
Input is either numeric or lowercase letters
{else}
Invalid input format
{/if}
```
### Practical Examples
**Email Validation:**
```smarty
{if $email matches "/^[^@\s]+@[^@\s]+\.[^@\s]+$/"}
Valid email address
{else}
Please enter a valid email address
{/if}
```
**Password Strength:**
```smarty
{if $password matches "/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/"}
Strong password
{else}
Password must contain uppercase, lowercase, numbers and be at least 8 characters
{/if}
```
**URL Validation:**
```smarty
{if $url matches "/^https?:\/\/(www\.)?[a-z0-9\-]+(\.[a-z]{2,})+/i"}
Valid URL format
{else}
Please enter a valid URL
{/if}
```
**Numeric Validation:**
```smarty
{if $input matches "/^[0-9]+$/"}
Valid numeric input
{else}
Please enter numbers only
{/if}
```
### Notes
- The `matches` operator uses PHP's `preg_match()` function internally
- Pattern delimiters must be valid regex delimiters (typically `/`)
- Invalid patterns will cause PHP warnings but won't break template execution
- For complex regex patterns, consider using variables for better readability
## Ternary
You can use the `?:` (or ternary) operator to test one expression and present the value
@@ -47,11 +47,13 @@ available within the included template.
## Option Flags
| Name | Description |
|---------|--------------------------------------------------------------------------------------|
| nocache | Disables caching of this subtemplate |
| caching | Enable caching of this subtemplate |
| inline | If set, merge the compile-code of the subtemplate into the compiled calling template |
| Name | Description |
|-----------|--------------------------------------------------------------------------------------|
| nocache | Disables caching of this subtemplate |
| caching | Enable caching of this subtemplate |
| inline \* | If set, merge the compile-code of the subtemplate into the compiled calling template |
\* The `inline` option flag is currently not implemented in Smarty v5. Using it will not trigger an error, however.
## Examples
```smarty
@@ -27,6 +27,13 @@ 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,6 +28,13 @@ 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,6 +47,14 @@ 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,6 +47,14 @@ 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,6 +31,15 @@ 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,6 +24,14 @@ 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
@@ -123,4 +123,89 @@ this will output:
```html
name: Zaphod Beeblebrox<br />
email: zaphod@slartibartfast.example.com<br />
```
```
## Backed Enums (PHP 8.1+)
Smarty supports accessing properties of [backed enums](https://www.php.net/manual/en/language.enumerations.backed.php) introduced in PHP 8.1.
### Accessing Enum Properties
You can access the `name` and `value` properties of backed enum cases:
```smarty
{* Access enum case properties *}
<option id="{MyEnum::Foo->name}">{MyEnum::Foo->value}</option>
```
### Complete Example
```php
<?php
use Smarty\Smarty;
// Define a backed enum
enum Status: string {
case Active = 'active';
case Inactive = 'inactive';
case Pending = 'pending';
}
$smarty = new Smarty();
$smarty->assign('currentStatus', Status::Active);
$smarty->display('template.tpl');
```
`template.tpl`:
```smarty
{* Display enum properties *}
Current status: {$currentStatus->name} (value: {$currentStatus->value})
{* Use in HTML attributes *}
<select name="status">
<option value="{Status::Active->value}" {if $currentStatus->name === 'Active'}selected{/if}>Active</option>
<option value="{Status::Inactive->value}" {if $currentStatus->name === 'Inactive'}selected{/if}>Inactive</option>
<option value="{Status::Pending->value}" {if $currentStatus->name === 'Pending'}selected{/if}>Pending</option>
</select>
```
This would output:
```html
Current status: Active (value: active)
<select name="status">
<option value="active" selected>Active</option>
<option value="inactive">Inactive</option>
<option value="pending">Pending</option>
</select>
```
### Integer-backed Enums
Integer-backed enums work the same way:
```php
<?php
enum Priority: int {
case Low = 1;
case Medium = 2;
case High = 3;
}
$smarty->assign('priority', Priority::High);
```
```smarty
{* Access integer enum properties *}
Priority level: {$priority->value} ({$priority->name})
```
Output:
```html
Priority level: 3 (High)
```
> **Note**: Backed enum support requires PHP 8.1 or higher. The enum must be registered or available in the current namespace.
+2
View File
@@ -22,6 +22,8 @@ Some of Smarty's features:
- [Template Inheritance](api/inheritance.md) for
easy management of template content.
- [Plugin](api/extending/introduction.md) architecture
- Regex pattern matching with the [`matches`](designers/language-basic-syntax/language-syntax-operators.md#regex-matching-operator) operator
- Support for PHP 8.1+ [backed enums](designers/language-variables/language-assigned-variables.md#backed-enums-php-81)
## Separation of presentation from application code
- This means templates can certainly contain logic under the condition
+1 -1
View File
@@ -1,7 +1,7 @@
# Getting started
## Requirements
Smarty can be run with PHP 7.2 to PHP 8.4.
Smarty can be run with PHP 7.2 to PHP 8.5.
## Installation
Smarty can be installed with [Composer](https://getcomposer.org/).
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 343.3 174.5" style="enable-background:new 0 0 343.3 174.5;" xml:space="preserve">
<style type="text/css">
.st0{fill:#E60000;}
.st1{fill:#FFFFFF;}
</style>
<path class="st0" d="M333.8,0H9.5C4.3,0,0,4.3,0,9.5v155.4c0,5.3,4.3,9.5,9.5,9.5h324.3c5.3,0,9.5-4.3,9.5-9.5V9.5
C343.3,4.3,339,0,333.8,0z"/>
<g>
<path class="st1" d="M148.7,58.5c-5.2,0-4.7,4.3-4.7,9.5v38.6c0,5.3,4.3,9.5,9.5,9.5c5.2,0,9.5-4.3,9.5-9.5V81.5
C163,65.7,154,58.5,148.7,58.5L148.7,58.5z"/>
<path class="st1" d="M47.1,72.6c-5.2,0-4.7,4.3-4.7,9.5v24.5c0,5.2,4.3,9.5,9.5,9.5c5.2,0,9.5-4.3,9.5-9.5V95.5
C61.4,79.8,52.4,72.6,47.1,72.6L47.1,72.6z"/>
<path class="st1" d="M200.7,58.3c-7.4,0-13.8,6.9-15.2,11.4c-3-7.6-8.5-11.2-12.2-11.2c-5.2,0-4.7,4.3-4.7,9.5l0.1,9.5v29.1
c0,5.2,4.3,9.5,9.5,9.5c5.2,0,9.5-4.3,9.5-9.5V84.4c0-3.8,1.2-8.4,7-8.4c1.7,0,6.8,0,6.8,8.4v22.2c0,5.2,4.3,9.5,9.5,9.5
c5.2,0,9.5-4.3,9.5-9.5V79.8C220.5,66.5,212.9,58.3,200.7,58.3L200.7,58.3z"/>
<path class="st1" d="M130.8,58.4c-4.4,0-7.7,2.6-9.1,7.1l-6.1,19.9l-6.1-19.7c-1.4-4.6-5.1-7.4-9.6-7.4h-0.9
c-4.5,0-8.2,2.8-9.6,7.4l-5.4,17.5c-1.5-4.9-5.8-31.4-31.1-33c-8.7-0.5-11.2,4.7-12,7.6c-0.7,2.8-0.4,5.2,1.3,7.3
c1.7,2.2,5,2.5,8.9,4C61,73,63.6,81.8,65.9,87.3c0.8,1.9,6.4,20.8,6.4,20.8c1.8,5.2,5.4,8.1,10.2,8.1H83c4.8,0,8.4-2.9,10-7.8
l6.3-19.7l6.2,19.7c1.6,5,5.2,7.8,10,7.8h0.5c4.8,0,8.4-2.9,10.2-8L139.2,71l0-0.1c0.2-0.7,0.6-2.3,0.6-3.6
C139.8,62.3,135.9,58.4,130.8,58.4L130.8,58.4z"/>
<path class="st1" d="M276,99.9c-0.2-0.2-0.4-0.4-0.6-0.6l-14.1-14.4l11.3-9.6l0,0c1.9-1.8,4.1-4.3,4.1-8c0-4.3-3.2-8.7-8.6-8.7
c-3.6,0-5.9,1.9-8,3.9l-15,14.2c-1.6-12.5-9.2-18.4-14-18.4c-4.2,0-4.8,2.7-4.8,6.5c-0.1,1.1-0.1,2.4-0.1,3.7v38
c0,4.9,3.8,9.1,8.6,9.6c0.3,0,0.7,0.1,1,0.1c5.3,0,9.5-4.4,9.5-9.6v-7.5l3.3-3l14.5,16.4c0.6,0.6,1.3,1.3,2.1,1.9
c1.1,0.9,2.5,1.5,4.2,1.8c0.1,0,1.2,0.1,1.8,0.1c4.3-0.1,8.7-3.2,8.6-8.6C279.6,104.1,277.9,101.9,276,99.9L276,99.9z"/>
<circle class="st1" cx="293.3" cy="106.7" r="9.5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+68
View File
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="195mm"
height="43.093071mm"
viewBox="0 0 195 43.093071"
version="1.1"
id="svg1156"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1153" />
<g
id="layer1"
transform="translate(-6.9662436,-7.8467567)">
<g
id="g21305-1"
style="font-size:33.5139px;line-height:1.25;stroke-width:0.0700042"
transform="translate(1.0553387,0.93988827)">
<g
id="g21248-5">
<rect
style="font-size:33.5139px;line-height:1.25;fill:#107ef4;fill-opacity:1;stroke:none;stroke-width:0.0727743"
id="rect846-9-0-1-2-7-2-1-9"
width="43.093071"
height="43.093071"
x="5.9109049"
y="6.9068685"
ry="2.4179134" />
<path
d="M 22.234889,18.179756 V 36.734061 H 19.567887 V 18.179756 h -6.882585 v -2.35155 h 16.403494 v 2.35155 z"
style="font-size:31.75px;line-height:1.25;font-family:FreeSans;-inkscape-font-specification:FreeSans;fill:#ffffff;stroke-width:0.238978"
id="path16874-7-9-2-4" />
<path
d="m 41.725989,35.328866 v 1.806679 q -0.946355,0.258097 -1.634613,0.258097 -2.380228,0 -2.46626,-2.380228 -1.691969,2.380228 -4.674422,2.380228 -1.548582,0 -2.580969,-0.745614 v 6.395069 H 27.989497 V 21.707084 h 2.380228 v 10.639329 q 0,1.347839 0.831645,2.150808 0.831646,0.802968 2.23684,0.802968 1.864034,0 2.982454,-1.376517 1.11842,-1.405195 1.11842,-3.699389 v -8.517199 h 2.380227 v 12.503362 q 0,1.175775 1.290485,1.175775 0.229419,0 0.516193,-0.05736 z"
style="font-size:31.75px;line-height:1.25;font-family:FreeSans;-inkscape-font-specification:FreeSans;fill:#ffffff;stroke-width:0.238978"
id="path16871-6-1-9-9" />
</g>
<g
aria-label="Temma"
transform="matrix(0.91459055,0,0,0.9686482,-4.3308403,0.67015143)"
id="text87274-2-5-0"
style="font-size:39.8454px;line-height:1.25;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#000000;stroke-width:0.0205099">
<path
d="m 89.719297,15.597759 v 4.50253 h -10.24872 v 23.30956 h -5.578356 v -23.30956 h -10.24872 v -4.50253 z"
style="font-weight:600;font-family:Poppins;-inkscape-font-specification:'Poppins Semi-Bold';fill:#000000"
id="path7781-61-0-9" />
<path
d="m 109.19647,31.894528 q 0,1.195362 -0.15939,2.151652 H 92.899673 q 0.199227,2.390724 1.673504,3.745467 1.4743,1.354744 3.62597,1.354744 3.107933,0 4.422823,-2.669642 h 6.01666 q -0.95629,3.187632 -3.66578,5.259593 -2.70948,2.032115 -6.654173,2.032115 -3.18765,0 -5.737768,-1.394588 -2.51026,-1.434435 -3.944694,-4.024386 -1.394589,-2.589951 -1.394589,-5.97681 0,-3.426705 1.394589,-6.016656 1.394589,-2.589951 3.904849,-3.98454 2.510273,-1.394589 5.777613,-1.394589 3.147783,0 5.618193,1.354743 2.51026,1.354744 3.86501,3.865004 1.39459,2.470415 1.39459,5.697893 z m -5.77759,-1.593816 q -0.0398,-2.151652 -1.55397,-3.426705 -1.51411,-1.314898 -3.705613,-1.314898 -2.07197,0 -3.50643,1.275053 -1.394586,1.235207 -1.713349,3.46655 z"
style="font-weight:600;font-family:Poppins;-inkscape-font-specification:'Poppins Semi-Bold';fill:#000000"
id="path7783-8-3-1" />
<path
d="m 140.67428,21.016733 q 4.06423,0 6.53465,2.51026 2.51026,2.470415 2.51026,6.9331 v 12.949756 h -5.57836 V 31.217156 q 0,-2.589951 -1.3149,-3.944695 -1.3149,-1.394589 -3.58608,-1.394589 -2.27119,0 -3.62594,1.394589 -1.31489,1.354744 -1.31489,3.944695 v 12.192693 h -5.57836 V 31.217156 q 0,-2.589951 -1.3149,-3.944695 -1.3149,-1.394589 -3.58608,-1.394589 -2.31104,0 -3.66578,1.394589 -1.3149,1.354744 -1.3149,3.944695 v 12.192693 h -5.57836 V 21.335496 h 5.57836 v 2.669642 q 1.07583,-1.394589 2.74933,-2.191497 1.71336,-0.796908 3.74547,-0.796908 2.58995,0 4.62207,1.115671 2.03211,1.075826 3.14778,3.107942 1.07583,-1.91258 3.10795,-3.068096 2.07196,-1.155517 4.46268,-1.155517 z"
style="font-weight:600;font-family:Poppins;-inkscape-font-specification:'Poppins Semi-Bold';fill:#000000"
id="path7785-7-6-7" />
<path
d="m 182.4322,21.016733 q 4.06423,0 6.53465,2.51026 2.51026,2.470415 2.51026,6.9331 v 12.949756 h -5.57836 V 31.217156 q 0,-2.589951 -1.31489,-3.944695 -1.3149,-1.394589 -3.58609,-1.394589 -2.27119,0 -3.62593,1.394589 -1.3149,1.354744 -1.3149,3.944695 v 12.192693 h -5.57836 V 31.217156 q 0,-2.589951 -1.31489,-3.944695 -1.3149,-1.394589 -3.58609,-1.394589 -2.31103,0 -3.66578,1.394589 -1.3149,1.354744 -1.3149,3.944695 v 12.192693 h -5.57835 V 21.335496 h 5.57835 v 2.669642 q 1.07583,-1.394589 2.74934,-2.191497 1.71335,-0.796908 3.74546,-0.796908 2.58996,0 4.62207,1.115671 2.03212,1.075826 3.14779,3.107942 1.07582,-1.91258 3.10794,-3.068096 2.07196,-1.155517 4.46268,-1.155517 z"
style="font-weight:600;font-family:Poppins;-inkscape-font-specification:'Poppins Semi-Bold';fill:#000000"
id="path7787-9-1-7" />
<path
d="m 195.34205,32.292982 q 0,-3.347014 1.3149,-5.936965 1.35474,-2.589951 3.62593,-3.98454 2.31104,-1.394589 5.14006,-1.394589 2.47041,0 4.3033,0.996135 1.87274,0.996135 2.98841,2.51026 v -3.147787 h 5.6182 v 22.074353 h -5.6182 v -3.227478 q -1.07583,1.553971 -2.98841,2.589952 -1.87273,0.996134 -4.34315,0.996134 -2.78917,0 -5.10021,-1.434434 -2.27119,-1.434434 -3.62593,-4.024385 -1.3149,-2.629797 -1.3149,-6.016656 z m 17.3726,0.07969 q 0,-2.032116 -0.79691,-3.46655 -0.79691,-1.47428 -2.15165,-2.231343 -1.35475,-0.796908 -2.90872,-0.796908 -1.55397,0 -2.86887,0.757063 -1.31489,0.757062 -2.15165,2.231342 -0.79691,1.434435 -0.79691,3.426705 0,1.99227 0.79691,3.506395 0.83676,1.47428 2.15165,2.271188 1.35475,0.796908 2.86887,0.796908 1.55397,0 2.90872,-0.757062 1.35474,-0.796908 2.15165,-2.231343 0.79691,-1.47428 0.79691,-3.506395 z"
style="font-weight:600;font-family:Poppins;-inkscape-font-specification:'Poppins Semi-Bold';fill:#000000"
id="path7789-2-0-1" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

+30 -1
View File
@@ -8,6 +8,25 @@ It allows you to write **templates**, using **variables**, **modifiers**, **func
<p>
The number of pixels is: {math equation="x * y" x=$height y=$width}.
</p>
<p>
{if $email matches "/^[^@]+@[^@]+\.[^@]+$/"}
Valid email address
{else}
Please enter a valid email
{/if}
</p>
```
```html
<h1>Hello world</h1>
<p>
The number of pixels is: 307200.
</p>
<p>
Valid email address
</p>
```
When this template is rendered, with the value "Hello world" for the variable $title, 640 for $width,
@@ -32,4 +51,14 @@ and 480 for $height, the result is:
- [Troubleshooting](./appendixes/troubleshooting.md)
## Source code
- [Smarty repository at GitHub](https://github.com/smarty-php/smarty)
- [Smarty repository at GitHub](https://github.com/smarty-php/smarty)
## Sponsors
### <img src="img/iwink-logo.svg" alt="iWink logo" width="100">
[iWink](https://www.iwink.nl/), fullservice digital agency
### <img src="img/temma-logo.svg" alt="Temma logo" width="100">
[Temma](https://www.temma.net/), the MVC framework based on Smarty
+14
View File
@@ -0,0 +1,14 @@
# 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,7 +20,6 @@
<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="mutagen-compose"
COMPOSE_CMD="docker compose"
$COMPOSE_CMD run --rm php72 ./run-tests.sh $@ && \
$COMPOSE_CMD run --rm php73 ./run-tests.sh $@ && \
+3 -9
View File
@@ -127,8 +127,6 @@ class File extends Base
&& (!function_exists('ini_get') || strlen(ini_get('opcache.restrict_api'))) < 1
) {
opcache_invalidate($_template->getCached()->filepath, true);
} elseif (function_exists('apc_compile_file')) {
apc_compile_file($_template->getCached()->filepath);
}
$cached = $_template->getCached();
$cached->timestamp = $cached->exists = is_file($cached->filepath);
@@ -145,7 +143,7 @@ class File extends Base
*
* @param Template $_template template object
*
* @return string content
* @return string|false content
*/
public function retrieveCachedContent(Template $_template)
{
@@ -223,10 +221,8 @@ class File extends Base
$_filepath = (string)$_file;
// directory ?
if ($_file->isDir()) {
if (!$_cache->isDot()) {
// delete folder if empty
@rmdir($_file->getPathname());
}
// delete folder if empty
@rmdir($_file->getPathname());
} else {
// delete only php files
if (substr($_filepath, -4) !== '.php') {
@@ -279,8 +275,6 @@ class File extends Base
&& (!function_exists('ini_get') || strlen(ini_get("opcache.restrict_api")) < 1)
) {
opcache_invalidate($_filepath, true);
} elseif (function_exists('apc_delete_file')) {
apc_delete_file($_filepath);
}
}
}
+144
View File
@@ -0,0 +1,144 @@
<?php
namespace Smarty\Compile;
/**
* This class handles compiling the attributes.
*/
class AttributeCompiler
{
/**
* Array of names of required attributes required by tag
*
* @var array
*/
protected $required_attributes = [];
/**
* Array of names of optional attribute required by tag
* use array('_any') if there is no restriction of attributes names
*
* @var array
*/
protected $optional_attributes = [];
/**
* Shorttag attribute order defined by its names
*
* @var array
*/
protected $shorttag_order = [];
/**
* Array of names of valid option flags
*
* @var array
*/
protected $option_flags = [];
public function __construct(
array $required_attributes = [],
array $optional_attributes = [],
array $shorttag_order = [],
array $option_flags = []
) {
$this->required_attributes = $required_attributes;
$this->optional_attributes = $optional_attributes;
$this->shorttag_order = $shorttag_order;
$this->option_flags = $option_flags;
}
/**
* This function checks if the attributes passed are valid
* The attributes passed for the tag to compile are checked against the list of required and
* optional attributes. Required attributes must be present. Optional attributes are check against
* the corresponding list. The keyword '_any' specifies that any attribute will be accepted
* as valid
*
* @param object $compiler compiler object
* @param array $attributes attributes applied to the tag
*
* @return array of mapped attributes for further processing
*/
public function getAttributes($compiler, $attributes)
{
$_indexed_attr = [];
$options = array_fill_keys($this->option_flags, true);
foreach ($attributes as $key => $mixed) {
// shorthand ?
if (!is_array($mixed)) {
// options flag ?
if (isset($options[trim($mixed, '\'"')])) {
$_indexed_attr[trim($mixed, '\'"')] = true;
// shorthand attribute ?
} elseif (isset($this->shorttag_order[$key])) {
$_indexed_attr[$this->shorttag_order[$key]] = $mixed;
} else {
// too many shorthands
$compiler->trigger_template_error('too many shorthand attributes', null, true);
}
// named attribute
} else {
foreach ($mixed as $k => $v) {
// options flag?
if (isset($options[$k])) {
if (is_bool($v)) {
$_indexed_attr[$k] = $v;
} else {
if (is_string($v)) {
$v = trim($v, '\'" ');
}
// Mapping array for boolean option value
static $optionMap = [1 => true, 0 => false, 'true' => true, 'false' => false];
if (isset($optionMap[$v])) {
$_indexed_attr[$k] = $optionMap[$v];
} else {
$compiler->trigger_template_error(
"illegal value '" . var_export($v, true) .
"' for options flag '{$k}'",
null,
true
);
}
}
// must be named attribute
} else {
$_indexed_attr[$k] = $v;
}
}
}
}
// check if all required attributes present
foreach ($this->required_attributes as $attr) {
if (!isset($_indexed_attr[$attr])) {
$compiler->trigger_template_error("missing '{$attr}' attribute", null, true);
}
}
// check for not allowed attributes
if ($this->optional_attributes !== ['_any']) {
$allowedAttributes = array_fill_keys(
array_merge(
$this->required_attributes,
$this->optional_attributes,
$this->option_flags
),
true
);
foreach ($_indexed_attr as $key => $dummy) {
if (!isset($allowedAttributes[$key]) && $key !== 0) {
$compiler->trigger_template_error("unexpected '{$key}' attribute", null, true);
}
}
}
// default 'false' for all options flags not set
foreach ($this->option_flags as $flag) {
if (!isset($_indexed_attr[$flag])) {
$_indexed_attr[$flag] = false;
}
}
return $_indexed_attr;
}
}
+6 -78
View File
@@ -82,84 +82,12 @@ abstract class Base implements CompilerInterface {
* @return array of mapped attributes for further processing
*/
protected function getAttributes($compiler, $attributes) {
$_indexed_attr = [];
$options = array_fill_keys($this->option_flags, true);
foreach ($attributes as $key => $mixed) {
// shorthand ?
if (!is_array($mixed)) {
// options flag ?
if (isset($options[trim($mixed, '\'"')])) {
$_indexed_attr[trim($mixed, '\'"')] = true;
// shorthand attribute ?
} elseif (isset($this->shorttag_order[$key])) {
$_indexed_attr[$this->shorttag_order[$key]] = $mixed;
} else {
// too many shorthands
$compiler->trigger_template_error('too many shorthand attributes', null, true);
}
// named attribute
} else {
foreach ($mixed as $k => $v) {
// options flag?
if (isset($options[$k])) {
if (is_bool($v)) {
$_indexed_attr[$k] = $v;
} else {
if (is_string($v)) {
$v = trim($v, '\'" ');
}
// Mapping array for boolean option value
static $optionMap = [1 => true, 0 => false, 'true' => true, 'false' => false];
if (isset($optionMap[$v])) {
$_indexed_attr[$k] = $optionMap[$v];
} else {
$compiler->trigger_template_error(
"illegal value '" . var_export($v, true) .
"' for options flag '{$k}'",
null,
true
);
}
}
// must be named attribute
} else {
$_indexed_attr[$k] = $v;
}
}
}
}
// check if all required attributes present
foreach ($this->required_attributes as $attr) {
if (!isset($_indexed_attr[$attr])) {
$compiler->trigger_template_error("missing '{$attr}' attribute", null, true);
}
}
// check for not allowed attributes
if ($this->optional_attributes !== ['_any']) {
$allowedAttributes = array_fill_keys(
array_merge(
$this->required_attributes,
$this->optional_attributes,
$this->option_flags
),
true
);
foreach ($_indexed_attr as $key => $dummy) {
if (!isset($allowedAttributes[$key]) && $key !== 0) {
$compiler->trigger_template_error("unexpected '{$key}' attribute", null, true);
}
}
}
// default 'false' for all options flags not set
foreach ($this->option_flags as $flag) {
if (!isset($_indexed_attr[$flag])) {
$_indexed_attr[$flag] = false;
}
}
return $_indexed_attr;
return (new AttributeCompiler(
$this->required_attributes,
$this->optional_attributes,
$this->shorttag_order,
$this->option_flags
))->getAttributes($compiler, $attributes);
}
/**
+20 -16
View File
@@ -3,24 +3,18 @@
* Smarty Internal Plugin Compile Registered Function
* Compiles code for the execution of a registered function
*
* @author Uwe Tews
*/
namespace Smarty\Compile;
use Smarty\Compiler\Template;
use Smarty\CompilerException;
use Smarty\FunctionHandler\AttributeFunctionHandlerInterface;
/**
* Smarty Internal Plugin Compile Registered Function Class
*
*/
class FunctionCallCompiler extends Base {
/**
* Attribute definition: Overwrites base class.
*
@@ -51,17 +45,27 @@ class FunctionCallCompiler extends Base {
*/
public function compile($args, Template $compiler, $parameter = [], $tag = null, $function = null): string
{
// check and get attributes
$_attr = $this->getAttributes($compiler, $args);
unset($_attr['nocache']);
$_paramsArray = $this->formatParamsArray($_attr);
$_params = 'array(' . implode(',', $_paramsArray) . ')';
if ($functionHandler = $compiler->getSmarty()->getFunctionHandler($function)) {
$attribute_overrides = [];
if ($functionHandler instanceof AttributeFunctionHandlerInterface) {
$attribute_overrides = $functionHandler->getSupportedAttributes();
}
// check and get attributes
$_attr = (new AttributeCompiler(
$attribute_overrides['required_attributes'] ?? $this->required_attributes,
$attribute_overrides['optional_attributes'] ?? $this->optional_attributes,
$attribute_overrides['shorttag_order'] ?? $this->shorttag_order,
$attribute_overrides['option_flags'] ?? $this->option_flags
))->getAttributes($compiler, $args);
unset($_attr['nocache']);
$_paramsArray = $this->formatParamsArray($_attr);
$_params = 'array(' . implode(',', $_paramsArray) . ')';
// not cacheable?
$compiler->tag_nocache = $compiler->tag_nocache || !$functionHandler->isCacheable();
$output = "\$_smarty_tpl->getSmarty()->getFunctionHandler(" . var_export($function, true) . ")";
+15 -12
View File
@@ -51,8 +51,9 @@ class ForTag extends Base {
$var = $_statement['var'];
$index = '';
}
$output .= "\$_smarty_tpl->assign($var, null);\n";
$output .= "\$_smarty_tpl->tpl_vars[$var]->value{$index} = {$_statement['value']};\n";
$itemVar = "\$_smarty_tpl->getVariable({$var})";
$output .= "\$_smarty_tpl->assign($var, []);\n";
$output .= "{$itemVar}->value{$index} = {$_statement['value']};\n";
}
if (is_array($_attr['var'])) {
$var = $_attr['var']['var'];
@@ -61,7 +62,8 @@ class ForTag extends Base {
$var = $_attr['var'];
$index = '';
}
$output .= "if ($_attr[ifexp]) {\nfor (\$_foo=true;$_attr[ifexp]; \$_smarty_tpl->tpl_vars[$var]->value{$index}$_attr[step]) {\n";
$itemVar = "\$_smarty_tpl->getVariable({$var})";
$output .= "if ($_attr[ifexp]) {\nfor (\$_foo=true;$_attr[ifexp]; {$itemVar}->value{$index}$_attr[step]) {\n";
} else {
$_statement = $_attr['start'];
if (is_array($_statement['var'])) {
@@ -71,21 +73,22 @@ class ForTag extends Base {
$var = $_statement['var'];
$index = '';
}
$output .= "\$_smarty_tpl->assign($var, null);";
$itemVar = "\$_smarty_tpl->getVariable({$var})";
$output .= "\$_smarty_tpl->assign($var, []);";
if (isset($_attr['step'])) {
$output .= "\$_smarty_tpl->tpl_vars[$var]->step = $_attr[step];";
$output .= "{$itemVar}->step = $_attr[step];";
} else {
$output .= "\$_smarty_tpl->tpl_vars[$var]->step = 1;";
$output .= "{$itemVar}->step = 1;";
}
if (isset($_attr['max'])) {
$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";
$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";
} else {
$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 .= "{$itemVar}->total = (int) ceil(({$itemVar}->step > 0 ? $_attr[to]+1 - ($_statement[value]) : $_statement[value]-($_attr[to])+1)/abs({$itemVar}->step));\n";
}
$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 .= "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 .= '?>';
+2 -2
View File
@@ -320,7 +320,7 @@ class DefaultExtension extends Base {
break;
}
foreach ($var as $curr_key => $curr_val) {
$results .= '<br>' . str_repeat('&nbsp;', $depth * 2) . '<b>' . strtr($curr_key, $_replace) .
$results .= '<br>' . str_repeat('&nbsp;', $depth * 2) . '<b>' . htmlspecialchars(strtr($curr_key, $_replace)) .
'</b> =&gt; ' .
$this->smarty_modifier_debug_print_var($curr_val, $max, $length, ++$depth, $objects);
$depth--;
@@ -338,7 +338,7 @@ class DefaultExtension extends Base {
}
$objects[] = $var;
foreach ($object_vars as $curr_key => $curr_val) {
$results .= '<br>' . str_repeat('&nbsp;', $depth * 2) . '<b> -&gt;' . strtr($curr_key, $_replace) .
$results .= '<br>' . str_repeat('&nbsp;', $depth * 2) . '<b> -&gt;' . htmlspecialchars(strtr($curr_key, $_replace)) .
'</b> = ' . $this->smarty_modifier_debug_print_var($curr_val, $max, $length, ++$depth, $objects);
$depth--;
}
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace Smarty\FunctionHandler;
use Smarty\Template;
/**
* Abstract implementation for function handlers which support custom attributes
*/
abstract class AttributeBase implements AttributeFunctionHandlerInterface
{
/**
* Array of names of required attribute required by tag
*
* @var array
*/
protected array $required_attributes = [];
/**
* Array of names of optional attribute required by tag
* use array('_any') if there is no restriction of attributes names
*
* @var array
*/
protected array $optional_attributes = [];
/**
* Shorttag attribute order defined by its names
*
* @var array
*/
protected array $shorttag_order = [];
/**
* Array of names of valid option flags
*
* @var array
*/
protected array $option_flags = [];
/**
* Return whether the output is cacheable.
* @var bool
*/
protected bool $cacheable = true;
/**
* Return whether the output is cacheable.
* @return bool
*/
public function isCacheable(): bool
{
return $this->cacheable;
}
/**
* Function body
* @param mixed $params The supplied parameters.
* @param Smarty\Template $template
* @return mixed
*/
abstract public function handle($params, Template $template): ?string;
/**
* Return the support attributes for this function.
* @return array<string, array>
*/
public function getSupportedAttributes(): array
{
return [
'required_attributes' => $this->required_attributes,
'optional_attributes' => $this->optional_attributes,
'shorttag_order' => $this->shorttag_order,
'option_flags' => $this->option_flags,
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace Smarty\FunctionHandler;
/**
* Function handler interface with support for specifying supported properties
*/
interface AttributeFunctionHandlerInterface extends FunctionHandlerInterface
{
/**
* Returns an array with the supported attributes, flags, and shorttags
* @return array<string, array>
*/
public function getSupportedAttributes(): array;
}
+16 -1
View File
@@ -189,7 +189,22 @@ class Fetch extends Base {
return;
}
} else {
$content = @file_get_contents($params['file']);
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']);
}
if ($content === false) {
throw new Exception("{fetch} cannot read resource '" . $params['file'] . "'");
}
+8 -3
View File
@@ -65,7 +65,7 @@ class HtmlImage extends Base {
break;
case 'link':
case 'href':
$prefix = '<a href="' . $_val . '">';
$prefix = '<a href="' . smarty_function_escape_special_chars($_val) . '">';
$suffix = '</a>';
break;
default:
@@ -143,7 +143,12 @@ class HtmlImage extends Base {
$width = round($width * $_resize);
$height = round($height * $_resize);
}
return $prefix . '<img src="' . $path_prefix . $file . '" alt="' . $alt . '" width="' . $width . '" height="' .
$height . '"' . $extra . ' />' . $suffix;
// $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;
}
}
+7 -3
View File
@@ -120,9 +120,6 @@ 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':
@@ -140,6 +137,13 @@ 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':
+44 -38
View File
@@ -160,6 +160,7 @@ class TemplateLexer
'LOGOP' => '"<", "==" ... logical operator',
'TLOGOP' => '"lt", "eq" ... logical operator; "is div by" ... if condition',
'SCOND' => '"is even" ... if condition',
'MATCHES' => '"matches" regex operator',
);
/**
@@ -567,7 +568,7 @@ class TemplateLexer
public function yylex3()
{
if (!isset($this->yy_global_pattern3)) {
$this->yy_global_pattern3 = $this->replace("/\G(\\s*SMARTYrdel)|\G((SMARTYldel)SMARTYal)|\G([\"])|\G('[^'\\\\]*(?:\\\\.[^'\\\\]*)*')|\G([$][0-9]*[a-zA-Z_]\\w*)|\G([$])|\G(\\s+is\\s+(not\\s+)?in\\s+)|\G(\\s+as\\s+)|\G(\\s+to\\s+)|\G(\\s+step\\s+)|\G(\\s+instanceof\\s+)|\G(\\s*([!=][=]{1,2}|[<][=>]?|[>][=]?|[&|]{2})\\s*)|\G(\\s+(eq|ne|neq|gt|ge|gte|lt|le|lte|mod|and|or|xor)\\s+)|\G(\\s+is\\s+(not\\s+)?(odd|even|div)\\s+by\\s+)|\G(\\s+is\\s+(not\\s+)?(odd|even))|\G([!]\\s*|not\\s+)|\G([(](int(eger)?|bool(ean)?|float|double|real|string|binary|array|object)[)]\\s*)|\G(\\s*[(]\\s*)|\G(\\s*[)])|\G(\\[\\s*)|\G(\\s*\\])|\G(\\s*[-][>]\\s*)|\G(\\s*[=][>]\\s*)|\G(\\s*[=]\\s*)|\G(([+]|[-]){2})|\G(\\s*([+]|[-])\\s*)|\G(\\s*([*]{1,2}|[%\/^&]|[<>]{2})\\s*)|\G([@])|\G(array\\s*[(]\\s*)|\G([#])|\G(\\s+[0-9]*[a-zA-Z_][a-zA-Z0-9_\-:]*\\s*[=]\\s*)|\G(([0-9]*[a-zA-Z_]\\w*)?(\\\\[0-9]*[a-zA-Z_]\\w*)+)|\G([0-9]*[a-zA-Z_]\\w*)|\G(\\d+)|\G([`])|\G([|][@]?)|\G([.])|\G(\\s*[,]\\s*)|\G(\\s*[;]\\s*)|\G([:]{2})|\G(\\s*[:]\\s*)|\G(\\s*[?]\\s*)|\G(0[xX][0-9a-fA-F]+)|\G(\\s+)|\G([\S\s])/isS");
$this->yy_global_pattern3 = $this->replace("/\G(\\s*SMARTYrdel)|\G((SMARTYldel)SMARTYal)|\G([\"])|\G('[^'\\\\]*(?:\\\\.[^'\\\\]*)*')|\G([$][0-9]*[a-zA-Z_]\\w*)|\G([$])|\G(\\s+is\\s+(not\\s+)?in\\s+)|\G(\\s+matches\\s+)|\G(\\s+as\\s+)|\G(\\s+to\\s+)|\G(\\s+step\\s+)|\G(\\s+instanceof\\s+)|\G(\\s*([!=][=]{1,2}|[<][=>]?|[>][=]?|[&|]{2})\\s*)|\G(\\s+(eq|ne|neq|gt|ge|gte|lt|le|lte|mod|and|or|xor)\\s+)|\G(\\s+is\\s+(not\\s+)?(odd|even|div)\\s+by\\s+)|\G(\\s+is\\s+(not\\s+)?(odd|even))|\G([!]\\s*|not\\s+)|\G([(](int(eger)?|bool(ean)?|float|double|real|string|binary|array|object)[)]\\s*)|\G(\\s*[(]\\s*)|\G(\\s*[)])|\G(\\[\\s*)|\G(\\s*\\])|\G(\\s*[-][>]\\s*)|\G(\\s*[=][>]\\s*)|\G(\\s*[=]\\s*)|\G(([+]|[-]){2})|\G(\\s*([+]|[-])\\s*)|\G(\\s*([*]{1,2}|[%\/^&]|[<>]{2})\\s*)|\G([@])|\G(array\\s*[(]\\s*)|\G([#])|\G(\\s+[0-9]*[a-zA-Z_][a-zA-Z0-9_\-:]*\\s*[=]\\s*)|\G(([0-9]*[a-zA-Z_]\\w*)?(\\\\[0-9]*[a-zA-Z_]\\w*)+)|\G([0-9]*[a-zA-Z_]\\w*)|\G(\\d+)|\G([`])|\G([|][@]?)|\G([.])|\G(\\s*[,]\\s*)|\G(\\s*[;]\\s*)|\G([:]{2})|\G(\\s*[:]\\s*)|\G(\\s*[?]\\s*)|\G(0[xX][0-9a-fA-F]+)|\G(\\s+)|\G([\S\s])/isS");
}
if (!isset($this->dataLength)) {
$this->dataLength = strlen($this->data);
@@ -662,119 +663,124 @@ class TemplateLexer
public function yy_r3_10()
{
$this->token = \Smarty\Parser\TemplateParser::TP_AS;
$this->token = \Smarty\Parser\TemplateParser::TP_MATCHES;
}
public function yy_r3_11()
{
$this->token = \Smarty\Parser\TemplateParser::TP_TO;
$this->token = \Smarty\Parser\TemplateParser::TP_AS;
}
public function yy_r3_12()
{
$this->token = \Smarty\Parser\TemplateParser::TP_STEP;
$this->token = \Smarty\Parser\TemplateParser::TP_TO;
}
public function yy_r3_13()
{
$this->token = \Smarty\Parser\TemplateParser::TP_INSTANCEOF;
$this->token = \Smarty\Parser\TemplateParser::TP_STEP;
}
public function yy_r3_14()
{
$this->token = \Smarty\Parser\TemplateParser::TP_INSTANCEOF;
}
public function yy_r3_15()
{
$this->token = \Smarty\Parser\TemplateParser::TP_LOGOP;
}
public function yy_r3_16()
public function yy_r3_17()
{
$this->token = \Smarty\Parser\TemplateParser::TP_SLOGOP;
}
public function yy_r3_18()
public function yy_r3_19()
{
$this->token = \Smarty\Parser\TemplateParser::TP_TLOGOP;
}
public function yy_r3_21()
public function yy_r3_22()
{
$this->token = \Smarty\Parser\TemplateParser::TP_SINGLECOND;
}
public function yy_r3_24()
public function yy_r3_25()
{
$this->token = \Smarty\Parser\TemplateParser::TP_NOT;
}
public function yy_r3_25()
public function yy_r3_26()
{
$this->token = \Smarty\Parser\TemplateParser::TP_TYPECAST;
}
public function yy_r3_29()
public function yy_r3_30()
{
$this->token = \Smarty\Parser\TemplateParser::TP_OPENP;
}
public function yy_r3_30()
public function yy_r3_31()
{
$this->token = \Smarty\Parser\TemplateParser::TP_CLOSEP;
}
public function yy_r3_31()
public function yy_r3_32()
{
$this->token = \Smarty\Parser\TemplateParser::TP_OPENB;
}
public function yy_r3_32()
public function yy_r3_33()
{
$this->token = \Smarty\Parser\TemplateParser::TP_CLOSEB;
}
public function yy_r3_33()
public function yy_r3_34()
{
$this->token = \Smarty\Parser\TemplateParser::TP_PTR;
}
public function yy_r3_34()
public function yy_r3_35()
{
$this->token = \Smarty\Parser\TemplateParser::TP_APTR;
}
public function yy_r3_35()
public function yy_r3_36()
{
$this->token = \Smarty\Parser\TemplateParser::TP_EQUAL;
}
public function yy_r3_36()
public function yy_r3_37()
{
$this->token = \Smarty\Parser\TemplateParser::TP_INCDEC;
}
public function yy_r3_38()
public function yy_r3_39()
{
$this->token = \Smarty\Parser\TemplateParser::TP_UNIMATH;
}
public function yy_r3_40()
public function yy_r3_41()
{
$this->token = \Smarty\Parser\TemplateParser::TP_MATH;
}
public function yy_r3_42()
public function yy_r3_43()
{
$this->token = \Smarty\Parser\TemplateParser::TP_AT;
}
public function yy_r3_43()
public function yy_r3_44()
{
$this->token = \Smarty\Parser\TemplateParser::TP_ARRAYOPEN;
}
public function yy_r3_44()
public function yy_r3_45()
{
$this->token = \Smarty\Parser\TemplateParser::TP_HATCH;
}
public function yy_r3_45()
public function yy_r3_46()
{
// resolve conflicts with shorttag and right_delimiter starting with '='
@@ -786,73 +792,73 @@ class TemplateLexer
$this->token = \Smarty\Parser\TemplateParser::TP_ATTR;
}
}
public function yy_r3_46()
public function yy_r3_47()
{
$this->token = \Smarty\Parser\TemplateParser::TP_NAMESPACE;
}
public function yy_r3_49()
public function yy_r3_50()
{
$this->token = \Smarty\Parser\TemplateParser::TP_ID;
}
public function yy_r3_50()
public function yy_r3_51()
{
$this->token = \Smarty\Parser\TemplateParser::TP_INTEGER;
}
public function yy_r3_51()
public function yy_r3_52()
{
$this->token = \Smarty\Parser\TemplateParser::TP_BACKTICK;
$this->yypopstate();
}
public function yy_r3_52()
public function yy_r3_53()
{
$this->token = \Smarty\Parser\TemplateParser::TP_VERT;
}
public function yy_r3_53()
public function yy_r3_54()
{
$this->token = \Smarty\Parser\TemplateParser::TP_DOT;
}
public function yy_r3_54()
public function yy_r3_55()
{
$this->token = \Smarty\Parser\TemplateParser::TP_COMMA;
}
public function yy_r3_55()
public function yy_r3_56()
{
$this->token = \Smarty\Parser\TemplateParser::TP_SEMICOLON;
}
public function yy_r3_56()
public function yy_r3_57()
{
$this->token = \Smarty\Parser\TemplateParser::TP_DOUBLECOLON;
}
public function yy_r3_57()
public function yy_r3_58()
{
$this->token = \Smarty\Parser\TemplateParser::TP_COLON;
}
public function yy_r3_58()
public function yy_r3_59()
{
$this->token = \Smarty\Parser\TemplateParser::TP_QMARK;
}
public function yy_r3_59()
public function yy_r3_60()
{
$this->token = \Smarty\Parser\TemplateParser::TP_HEX;
}
public function yy_r3_60()
public function yy_r3_61()
{
$this->token = \Smarty\Parser\TemplateParser::TP_SPACE;
}
public function yy_r3_61()
public function yy_r3_62()
{
$this->token = \Smarty\Parser\TemplateParser::TP_TEXT;
+5
View File
@@ -160,6 +160,7 @@ class TemplateLexer
'LOGOP' => '"<", "==" ... logical operator',
'TLOGOP' => '"lt", "eq" ... logical operator; "is div by" ... if condition',
'SCOND' => '"is even" ... if condition',
'MATCHES' => '"matches" regex operator',
);
/**
@@ -325,6 +326,7 @@ class TemplateLexer
tlop = ~\s+is\s+(not\s+)?(odd|even|div)\s+by\s+~
scond = ~\s+is\s+(not\s+)?(odd|even)~
isin = ~\s+is\s+(not\s+)?in\s+~
matches = ~\s+matches\s+~
as = ~\s+as\s+~
to = ~\s+to\s+~
step = ~\s+step\s+~
@@ -469,6 +471,9 @@ class TemplateLexer
isin {
$this->token = \Smarty\Parser\TemplateParser::TP_ISIN;
}
matches {
$this->token = \Smarty\Parser\TemplateParser::TP_MATCHES;
}
as {
$this->token = \Smarty\Parser\TemplateParser::TP_AS;
}
+1206 -1136
View File
File diff suppressed because it is too large Load Diff
+30 -6
View File
@@ -681,6 +681,11 @@ expr(res) ::= expr(e1) isin(c) value(v). {
res = c . e1.',(array)'.v.')';
}
// regex matching
expr(res) ::= expr(e1) matchop(c) value(e2). {
res = c . e2 . ',' . e1 . ') ';
}
// null coalescing
nullcoalescing(res) ::= expr(v) QMARK QMARK expr(e2). {
res = v.' ?? '.e2;
@@ -1062,12 +1067,22 @@ object(res) ::= varindexed(vi) objectchain(oc). {
}
}
// optional objectchain - empty
optobjectchain(res) ::= . {
res = '';
}
// optional objectchain - present
optobjectchain(res) ::= objectchain(oc). {
res = oc;
}
// single element
objectchain(res) ::= objectelement(oe). {
res = oe;
}
// chain of elements
// chain of elements
objectchain(res) ::= objectchain(oc) objectelement(oe). {
res = oc.oe;
}
@@ -1111,7 +1126,7 @@ objectelement(res)::= PTR method(f). {
//
// function
//
function(res) ::= ns1(f) OPENP variablelist(v) CLOSEP. {
function(res) ::= ns1(f) OPENP variablelist(v) CLOSEP optobjectchain(oc). {
if (f == 'isset') {
res = '(true';
@@ -1125,15 +1140,15 @@ function(res) ::= ns1(f) OPENP variablelist(v) CLOSEP. {
res .= ' && (' . $value . ' !== null)';
}
}
res .= ')';
res .= ')' . oc;
} elseif (f == 'empty') {
if (count(v) != 1) {
throw new CompilerException("Invalid number of arguments for empty. empty expects at exactly one parameter.");
}
if (is_array(v[0])) {
res .= '( !' . v[0][0] . ' || empty(' . v[0][1] . '))';
res = '( !' . v[0][0] . ' || empty(' . v[0][1] . '))' . oc;
} else {
res = 'false == ' . v[0];
res = 'false == ' . v[0] . oc;
}
} else {
$p = array();
@@ -1144,7 +1159,7 @@ function(res) ::= ns1(f) OPENP variablelist(v) CLOSEP. {
$p[] = $value;
}
}
res = $this->compiler->compileModifierInExpression(f, $p);
res = $this->compiler->compileModifierInExpression(f, $p) . oc;
}
}
@@ -1244,6 +1259,11 @@ static_class_access(res) ::= ID(v). {
res = array(v, '');
}
// static class constant with object chain
static_class_access(res) ::= ID(v) objectchain(oc). {
res = array(v, oc);
}
// static class variables
static_class_access(res) ::= DOLLARID(v) arrayindex(a). {
res = array(v, a, 'property');
@@ -1303,6 +1323,10 @@ scond(res) ::= SINGLECOND(o). {
res = $scond[$op];
}
matchop(res) ::= MATCHES(o). {
res = 'preg_match(';
}
//
// ARRAY element assignment
//
+12 -1
View File
@@ -54,8 +54,19 @@ 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) {
+30 -4
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.
* If set to 'none' none is allowed.
* To disable access to all static classes set $static_classes = null.
*
* @var array
*/
@@ -206,7 +206,11 @@ class Security {
* @return boolean true if class is trusted
*/
public function isTrustedStaticClass($class_name, $compiler) {
if (isset($this->static_classes)
// 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)
&& (empty($this->static_classes) || in_array($class_name, $this->static_classes))
) {
return true;
@@ -474,12 +478,34 @@ class Security {
* @throws \Smarty\Exception
*/
private function _checkDir($filepath, $dirs) {
$directory = dirname($this->smarty->_realpath($filepath, true)) . DIRECTORY_SEPARATOR;
// 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 = [];
if (!preg_match('#[\\\\/][.][.][\\\\/]#', $directory)) {
while (true) {
// test if the directory is trusted
if (isset($dirs[$directory])) {
if (isset($trusted[$directory])) {
return $_directory;
}
// abort if we've reached root
+3 -7
View File
@@ -54,7 +54,7 @@ class Smarty extends \Smarty\TemplateBase {
/**
* smarty version
*/
const SMARTY_VERSION = '5.5.1';
const SMARTY_VERSION = '5.8.4';
/**
* define caching modes
@@ -1344,10 +1344,8 @@ class Smarty extends \Smarty\TemplateBase {
}
$_filepath = (string)$_file;
if ($_file->isDir()) {
if (!$_compile->isDot()) {
// delete folder if empty
@rmdir($_file->getPathname());
}
// delete folder if empty
@rmdir($_file->getPathname());
} else {
// delete only php files
if (substr($_filepath, -4) !== '.php') {
@@ -1385,8 +1383,6 @@ class Smarty extends \Smarty\TemplateBase {
&& (!function_exists('ini_get') || strlen(ini_get('opcache.restrict_api')) < 1)
) {
opcache_invalidate($_filepath, true);
} elseif (function_exists('apc_delete_file')) {
apc_delete_file($_filepath);
}
}
}
+6 -2
View File
@@ -260,7 +260,11 @@ class Template extends TemplateBase {
$tpl = $this->smarty->doCreateTemplate($template_name, $cache_id, $compile_id, $this, $caching, $cache_lifetime);
$tpl->inheritance = $this->getInheritance(); // re-use the same Inheritance object inside the inheritance tree
// 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;
if ($scope) {
$tpl->defaultScope = $scope;
@@ -531,7 +535,7 @@ class Template extends TemplateBase {
*/
public function getRightDelimiter()
{
return $this->right_delimiter ?? $this->getSmarty()->getRightDelimiter();;
return $this->right_delimiter ?? $this->getSmarty()->getRightDelimiter();
}
/**
-2
View File
@@ -251,8 +251,6 @@ class Compiled extends GeneratedPhpFile {
&& (!function_exists('ini_get') || strlen(ini_get("opcache.restrict_api")) < 1)
) {
opcache_invalidate($this->filepath, true);
} elseif (function_exists('apc_compile_file')) {
apc_compile_file($this->filepath);
}
}
if (defined('HHVM_VERSION')) {
+1 -1
View File
@@ -104,7 +104,7 @@ abstract class TemplateBase extends Data {
}
// register the object
$smarty->registered_objects[$object_name] =
[$object, (array)$allowed_methods_properties, (boolean)$format, (array)$block_methods];
[$object, (array)$allowed_methods_properties, (bool)$format, (array)$block_methods];
return $this;
}
-1
View File
@@ -15,4 +15,3 @@ if (!ini_get('date.timezone')) {
}
-1
View File
@@ -6,7 +6,6 @@
/*
* Smarty PHPUnit Config
*/
define('individualFolders', true);
define('MysqlCacheEnable', false);
define('PdoCacheEnable', false);
define('PdoGzipCacheEnable', false);
+129 -47
View File
@@ -49,6 +49,37 @@ class PHPUnit_Smarty extends PHPUnit\Framework\TestCase
*/
public static $cwd = null;
/**
* Temp directory base for this test class (compile, cache, templates_tmp)
*
* @var string|null
*/
private static $tempBase = null;
/**
* Unique token for the current test class's temp directory.
* Generated once per class.
*
* @var string|null
*/
private static $tempId = null;
/**
* Return the temp directory base for the current test class.
*
* @return string
* @throws \LogicException If the temp directory base has not been initialized yet.
*/
public static function getTempBase(): string
{
if (self::$tempBase === null) {
throw new \LogicException(
'Temp directory base has not been initialized. Call setUpSmarty() before using temp-path helpers.'
);
}
return self::$tempBase;
}
/**
* PDO object for Mysql tests
*
@@ -75,8 +106,37 @@ class PHPUnit_Smarty extends PHPUnit\Framework\TestCase
*/
public static function tearDownAfterClass(): void
{
//self::$pdo = null;
self::$testNumber = 0;
// Remove the unique temp directory for this test class unless the caller
// wants to inspect the artifacts (e.g. for debugging a failure).
if (!getenv('KEEP_SMARTY_TEST_ARTIFACTS') && self::$tempBase !== null && is_dir(self::$tempBase)) {
self::removeDir(self::$tempBase);
}
}
/**
* Recursively remove a directory, silently ignoring any errors.
*
* @param string $dir
*/
private static function removeDir(string $dir): void
{
$dir = rtrim($dir, DIRECTORY_SEPARATOR);
$items = @scandir($dir);
if ($items === false) {
return;
}
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $dir . DIRECTORY_SEPARATOR . $item;
if (is_dir($path) && !is_link($path)) {
self::removeDir($path);
} else {
@unlink($path);
}
}
@rmdir($dir);
}
/**
@@ -89,12 +149,46 @@ class PHPUnit_Smarty extends PHPUnit\Framework\TestCase
public function __construct($name = null, array $data = array(), $dataName = '')
{
date_default_timezone_set('Europe/Berlin');
if (!defined('individualFolders')) {
define('individualFolders', true);
}
parent::__construct($name, $data, $dataName);
}
/**
* Compute the temp directory base for a given test directory.
*
* Returns a path unique to this test class run under sys_get_temp_dir(),
* so that concurrent or sequential runs of different test classes never
* share compiled/cached output. The unique token is generated once per
* class lifetime and reset in tearDownAfterClass().
*
* Example:
* /path/to/smarty/tests/UnitTests/TagTests/If
* /tmp/smarty-tests/UnitTests/TagTests/If/<unique-id>/
*
* @param string $dir absolute test directory
* @return string absolute temp base directory (with trailing separator)
*/
private static function getTempDir($dir)
{
// Lazily generate a unique token for this test class.
if (self::$tempId === null) {
self::$tempId = uniqid('', true);
}
$testsRoot = realpath(__DIR__);
$realDir = realpath($dir) ?: $dir;
// compute relative path from tests/ root
if (strpos($realDir, $testsRoot) === 0) {
$relative = substr($realDir, strlen($testsRoot));
} else {
// fallback: use full path hash
$relative = DIRECTORY_SEPARATOR . md5($realDir);
}
return rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR)
. DIRECTORY_SEPARATOR . 'smarty-tests'
. $relative
. DIRECTORY_SEPARATOR . self::$tempId
. DIRECTORY_SEPARATOR;
}
/**
* Setup Smarty instance called for each test
*
@@ -102,47 +196,37 @@ class PHPUnit_Smarty extends PHPUnit\Framework\TestCase
*/
public function setUpSmarty($dir)
{
static $s_dir;
// set up current working directory
chdir($dir);
self::$cwd = getcwd();
// compute temp base for this test directory
self::$tempBase = self::getTempDir($dir);
// create missing folders for test
if (self::$init) {
if (!is_dir($dir . '/templates')) {
mkdir($dir . '/templates');
if (!is_dir(self::$tempBase . 'templates_c')) {
mkdir(self::$tempBase . 'templates_c', 0775, true);
}
if (!is_dir($dir . '/configs')) {
mkdir($dir . '/configs');
}
if (individualFolders != 'true') {
if (!isset($s_dir[ $dir ])) {
$this->cleanDir($dir . '/templates_c');
$this->cleanDir($dir . '/cache');
if (is_dir($dir . '/templates_tmp')) {
$this->cleanDir($dir . '/templates_tmp');
}
$s_dir[ $dir ] = true;
}
$dir = __DIR__;
}
if (!is_dir($dir . '/templates_c')) {
mkdir($dir . '/templates_c');
}
chmod($dir . '/templates_c', 0775);
if (!is_dir($dir . '/cache')) {
mkdir($dir . '/cache');
chmod($dir . '/cache', 0775);
if (!is_dir(self::$tempBase . 'cache')) {
mkdir(self::$tempBase . 'cache', 0775, true);
}
if (!is_dir(self::$tempBase . 'templates_tmp')) {
mkdir(self::$tempBase . 'templates_tmp', 0775, true);
}
self::$init = false;
}
clearstatcache();
// instance Smarty class
$this->smarty = new \Smarty\Smarty();
if (individualFolders != 'true') {
$this->smarty->setCompileDir(__DIR__ . '/templates_c');
$this->smarty->setCacheDir(__DIR__ . '/cache');
$this->smarty->setCompileDir(self::getTempBase() . 'templates_c');
$this->smarty->setCacheDir(self::getTempBase() . 'cache');
$this->smarty->addTemplateDir(self::getTempBase() . 'templates_tmp');
// Clean output dirs once at the start of each test class run
if (self::$testNumber === 0) {
$this->cleanDirs();
}
self::$testNumber++;
}
@@ -230,24 +314,24 @@ KEY `name` (`name`)
{
$this->cleanCompileDir();
$this->cleanCacheDir();
if (is_dir(self::$cwd . '/templates_tmp')) {
$this->cleanDir(self::$cwd . '/templates_tmp');
$templatesTmpDir = self::getTempBase() . 'templates_tmp';
if (is_dir($templatesTmpDir)) {
$this->cleanDir($templatesTmpDir);
}
$this->assertTrue(true);
}
}
/**
* Make temporary template file
*
*/
public function makeTemplateFile($name, $code)
protected function makeTemplateFile($name, $code)
{
if (!is_dir(self::$cwd . '/templates_tmp')) {
mkdir(self::$cwd . '/templates_tmp');
chmod(self::$cwd . '/templates_tmp', 0775);
}
$fileName = self::$cwd . '/templates_tmp/' . "{$name}";
file_put_contents($fileName, $code);
file_put_contents(self::getTempBase() . 'templates_tmp' . '/' . $name, $code);
}
protected function removeTemplateFile($name)
{
unlink(self::getTempBase() . 'templates_tmp' . '/' . $name);
}
/**
@@ -291,10 +375,8 @@ KEY `name` (`name`)
}
// directory ?
if ($file->isDir()) {
if (!$ri->isDot()) {
// delete folder if empty
@rmdir($file->getPathname());
}
// delete folder if empty
@rmdir($file->getPathname());
} else {
unlink($file->getPathname());
}
@@ -18,6 +18,7 @@ class TemplateNormalizationTest extends PHPUnit_Smarty
public function setUp(): void
{
$this->setUpSmarty(__DIR__);
$this->smarty->setTemplateDir(__DIR__ . '/templates');
}
public function testGetTemplateDir()
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -18,10 +18,6 @@ class UndefinedTemplateVarTest extends PHPUnit_Smarty
$this->setUpSmarty(__DIR__);
}
public function testInit()
{
$this->cleanDirs();
}
/**
* Test Error suppression template fetched by Smarty object
*/
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -21,10 +21,6 @@ class FilterTest extends PHPUnit_Smarty
$this->setUpSmarty(__DIR__);
}
public function testInit()
{
$this->cleanDirs();
}
/**
* test loaded filter
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -20,10 +20,6 @@ class GetterSetterTest extends PHPUnit_Smarty
$this->setUpSmarty(__DIR__);
}
public function testInit()
{
$this->cleanDirs();
}
/**
* test setter on Smarty object
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -24,10 +24,6 @@ class DefaultPluginHandlerTest extends PHPUnit_Smarty
}
public function testInit()
{
$this->cleanDirs();
}
public function testDefaultFunctionScript()
{
$this->assertEquals("scriptfunction foo bar", $this->smarty->fetch('test_default_function_script.tpl'));
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -21,10 +21,6 @@ class HttpModifiedSinceTest extends PHPUnit_Smarty
$this->setUpSmarty(__DIR__);
}
public function testInit()
{
$this->cleanDirs();
}
/**
*
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -1,31 +0,0 @@
<?php
/**
* Smarty PHPunit tests for cache resource Apc
*
* @author Uwe Tews
*/
include_once __DIR__ . '/../Memcache/CacheResourceCustomMemcacheTest.php';
include_once __DIR__ . '/../_shared/PHPunitplugins/cacheresource.apctest.php';
/**
* class for cache resource file tests
*
*
* @preserveGlobalState disabled
*
*/
class CacheResourceCustomApcTest extends CacheResourceCustomMemcacheTest
{
public function setUp(): void
{
if (!function_exists('apc_cache_info') || ini_get('apc.enable_cli')) {
$this->markTestSkipped('APC cache not available');
}
$this->setUpSmarty(__DIR__);
parent::setUp();
$this->smarty->setCachingType('apc');
$this->smarty->registerCacheResource('apc', new Smarty_CacheResource_Apctest());
}
}
@@ -25,11 +25,6 @@ class CacheResourceFileTest extends CacheResourceTestCommon
}
public function testInit()
{
$this->cleanDirs();
}
/**
* test getCachedFilepath with configuration['useSubDirs'] enabled
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -32,10 +32,6 @@ class CacheResourceCustomMemcacheTest extends CacheResourceTestCommon
$this->smarty->setCachingType('memcachetest');
}
public function testInit()
{
$this->cleanDirs();
}
protected function doClearCacheAssertion($a, $b)
{
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -29,15 +29,11 @@ if (MysqlCacheEnable == true) {
$this->getConnection();
}
$this->setUpSmarty(__DIR__);
$this->initMysqlCache();
parent::setUp();
$this->smarty->setCachingType('mysqltest');
}
public function testInit()
{
$this->cleanDirs();
$this->initMysqlCache();
}
}
}
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -28,15 +28,11 @@ class CacheResourceCustomPDOTest extends CacheResourceTestCommon
$this->getConnection();
}
$this->setUpSmarty(__DIR__);
$this->initMysqlCache();
parent::setUp();
$this->smarty->registerCacheResource('pdo',
new Smarty_CacheResource_Pdotest($this->getPDO(), 'output_cache'));
}
public function testInit()
{
$this->cleanDirs();
$this->initMysqlCache();
}
}
@@ -2,7 +2,7 @@
use Smarty\Template\Cached;
require_once __DIR__ . '/../../__shared/cacheresources/cacheresource.pdo.php';
require_once __DIR__ . '/../_shared/cacheresources/cacheresource.pdo.php';
class Smarty_CacheResource_Pdotest extends Smarty_CacheResource_Pdo
{
@@ -27,16 +27,12 @@ class CacheResourceCustomPDOGzipTest extends CacheResourceTestCommon
$this->getConnection();
}
$this->setUpSmarty(__DIR__);
$this->initMysqlCache();
parent::setUp();
$this->smarty->setCachingType('pdo');
$this->smarty->registerCacheResource('pdo', new Smarty_CacheResource_Pdo_Gziptest($this->getPDO(),
'output_cache'));
}
public function testInit()
{
$this->cleanDirs();
$this->initMysqlCache();
}
}
@@ -2,7 +2,7 @@
use Smarty\Template\Cached;
require_once __DIR__ . '/../../__shared/cacheresources/cacheresource.pdo_gzip.php';
require_once __DIR__ . '/../_shared/cacheresources/cacheresource.pdo_gzip.php';
class Smarty_CacheResource_Pdo_Gziptest extends Smarty_CacheResource_Pdo_Gzip
{
@@ -26,6 +26,7 @@ if (MysqlCacheEnable == true) {
$this->getConnection();
}
$this->setUpSmarty(__DIR__);
$this->initMysqlCache();
parent::setUp();
if (!class_exists('Smarty_CacheResource_Mysqltest', false)) {
require_once(__DIR__ . "/../_shared/PHPunitplugins/cacheresource.mysqltest.php");
@@ -34,11 +35,6 @@ if (MysqlCacheEnable == true) {
$this->smarty->registerCacheResource('foobar', new Smarty_CacheResource_Mysqltest());
}
public function testInit()
{
$this->cleanDirs();
$this->initMysqlCache();
}
}
}
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -1,33 +0,0 @@
<?php
use Smarty\Smarty;
use Smarty\Template;
use Smarty\Template\Cached;
require_once __DIR__ . '/../../../__shared/cacheresources/cacheresource.apc.php';
class Smarty_CacheResource_Apctest extends Smarty_CacheResource_Apc
{
public $lockTime = 0;
public function hasLock(Smarty $smarty, Cached $cached)
{
if ($this->lockTime) {
$this->lockTime--;
if (!$this->lockTime) {
$this->releaseLock($smarty, $cached);
}
}
return parent::hasLock($smarty, $cached);
}
public function get(Template $_template)
{
$this->contents = array();
$this->timestamps = array();
$t = $this->getContent($_template);
return $t ? $t : null;
}
}
@@ -3,7 +3,7 @@
use Smarty\Template;
use Smarty\Template\Cached;
require_once __DIR__ . '/../../../__shared/cacheresources/cacheresource.memcache.php';
require_once __DIR__ . '/../cacheresources/cacheresource.memcache.php';
class Smarty_CacheResource_Memcachetest extends Smarty_CacheResource_Memcache
{
@@ -3,7 +3,7 @@
use Smarty\Exception;
use Smarty\Template\Cached;
require_once __DIR__ . '/../../../__shared/cacheresources/cacheresource.mysql.php';
require_once __DIR__ . '/../cacheresources/cacheresource.mysql.php';
class Smarty_CacheResource_Mysqltest extends Smarty_CacheResource_Mysql
{
@@ -0,0 +1,193 @@
<?php
use Smarty\Compile\AttributeCompiler;
use Smarty\Compiler\Template;
class AttributeCompilerTest extends PHPUnit\Framework\TestCase
{
/**
* The template compiler.
*/
private $template_compiler;
/**
* The attributes
*/
private $attributes = [];
/**
* @inheritDoc
* Set up attribute compiler class
*/
protected function setUp(): void
{
$this->template_compiler = $this->createMock(Template::class);
// reset attributes to empty arrays
$this->attributes = [
'required_attributes' => [],
'optional_attributes' => [],
'shorttag_order' => [],
'option_flags' => [],
];
}
/**
* Create the attribute compiler for testing.
*/
private function createAttributeCompiler() {
return new AttributeCompiler(
$this->attributes['required_attributes'],
$this->attributes['optional_attributes'],
$this->attributes['shorttag_order'],
$this->attributes['option_flags']
);
}
/**
* Tests shorthand attribute compiling.
*/
public function testAttributeCompiler(): void
{
$this->attributes['shorttag_order'] = ['shorttag'];
$this->attributes['required_attributes'] = ['required'];
$this->attributes['option_flags'] = ['option', 'option_two'];
$payload = [
0 => 'shorttag value',
1 => [
'required' => 'required_value'
],
2 => 'option',
];
$this->assertEquals(
$this->createAttributeCompiler()
->getAttributes($this->template_compiler, $payload),
[
'shorttag' => 'shorttag value',
'required' => 'required_value',
'option' => true,
'option_two' => false,
]
);
}
/**
* Tests normal optional attribute compiling.
*/
public function testAttributeCompilerOptionalArguments(): void
{
$this->attributes['optional_attributes'] = ['optional'];
$payload = [
0 => [
'optional' => 'optional value'
],
];
$this->assertEquals(
$this->createAttributeCompiler()
->getAttributes($this->template_compiler, $payload),
[
'optional' => 'optional value',
]
);
$this->assertEquals(
$this->createAttributeCompiler()
->getAttributes($this->template_compiler, []),
[]
);
}
/**
* Tests any attribute compiling.
*/
public function testAttributeCompilerAnyOptionalArguments(): void
{
$this->attributes['optional_attributes'] = ['_any'];
$payload = [
0 => [
'optional' => 'optional value'
],
1 => [
'optional_two' => 'optional value two'
],
];
$this->assertEquals(
$this->createAttributeCompiler()
->getAttributes($this->template_compiler, $payload),
[
'optional' => 'optional value',
'optional_two' => 'optional value two',
]
);
}
/**
* Test if the attribute compiler tries to throw a too many shorthand attributes error.
*/
public function testAttributeCompilerTooManyShorthands(): void
{
$payload = [
0 => 'option one',
];
$this->template_compiler
->expects(self::once())
->method('trigger_template_error')
->with('too many shorthand attributes', null, true);
$this->createAttributeCompiler()
->getAttributes($this->template_compiler, $payload);
}
/**
* Test if the attribute compiler tries to throw a missing required attribute error.
*/
public function testAttributeCompilerWithMissingRequiredAttributes(): void
{
$this->attributes['required_attributes'] = ['required'];
$this->template_compiler
->expects(self::once())
->method('trigger_template_error')
->with('missing \'required\' attribute', null, true);
$this->createAttributeCompiler()
->getAttributes($this->template_compiler, []);
}
/**
* Test if the attribute compiler tries to throw a illegal value template error.
*/
public function testAttributeCompilerWithInvalidOptionAttribute(): void
{
$this->attributes['option_flags'] = ['option'];
$this->template_compiler
->expects(self::once())
->method('trigger_template_error')
->with('illegal value \'\'foo\'\' for options flag \'option\'', null, true);
$this->createAttributeCompiler()
->getAttributes($this->template_compiler, [0 => ['option' => 'foo']]);
}
/**
* Test if the attribute compiler tries to throw an unexpected attribute error.
*/
public function testAttributeCompilerWithInvalidUnexpectedAttribute(): void
{
$this->template_compiler
->expects(self::once())
->method('trigger_template_error')
->with('unexpected \'unexpected\' attribute', null, true);
$this->createAttributeCompiler()
->getAttributes($this->template_compiler, [0 => ['unexpected' => 'bar']]);
}
}
@@ -0,0 +1,62 @@
<?php
use Smarty\Compile\FunctionCallCompiler;
use Smarty\Compiler\Template;
use Smarty\FunctionHandler\AttributeFunctionHandlerInterface;
use Smarty\Smarty;
class FunctionCallCompilerTest extends PHPUnit\Framework\TestCase
{
/**
* @inheritDoc
* Set up attribute compiler class
*/
protected function setUp(): void
{
$this->smarty = $this->createMock(Smarty::class);
$this->template_compiler = $this->createMock(Template::class);
$this->template_compiler
->expects(self::once())
->method('getSmarty')
->willReturn($this->smarty);
}
public function testAttributeFunctionHandlerInterface(): void
{
$attribute_function_handler = $this->createMock(AttributeFunctionHandlerInterface::class);
$attribute_function_handler
->expects(self::once())
->method('getSupportedAttributes')
->willReturn([
'required_attributes' => ['required'],
'optional_attributes' => ['optional'],
'shorttag_order' => ['short'],
'option_flags' => ['option'],
]);
$args = [
0 => 'short',
1 => 'option',
2 => [
'optional' => 'optional',
],
3 => [
'required' => 'required',
],
];
$this->smarty
->expects(self::once())
->method('getFunctionHandler')
->with('method')
->willReturn($attribute_function_handler);
$function_call_compiler = new FunctionCallCompiler();
$this->assertEquals(
$function_call_compiler->compile($args, $this->template_compiler, [], null, 'method'),
'$_smarty_tpl->getSmarty()->getFunctionHandler(\'method\')->handle(array(\'short\'=>short,\'option\'=>1,\'optional\'=>optional,\'required\'=>required), $_smarty_tpl)'
);
}
}
@@ -20,10 +20,6 @@ class CompileCompilerPluginTest extends PHPUnit_Smarty
$this->setUpSmarty(__DIR__);
}
public function testInit()
{
$this->cleanDirs();
}
/**
* test compiler plugin tag in template file
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -1,2 +0,0 @@
# Ignore anything in here, but keep this directory
*
@@ -17,13 +17,9 @@ class AutoliteralTest extends PHPUnit_Smarty
public function setUp(): void
{
$this->setUpSmarty(__DIR__);
$this->smarty->addPluginsDir("../../__shared/PHPunitplugins/");
$this->smarty->addPluginsDir("./plugins/");
}
public function testInit()
{
$this->cleanDirs();
}
/**
* test '{ ' delimiter
@@ -20,10 +20,6 @@ class DelimiterTest extends PHPUnit_Smarty
$this->setUpSmarty(__DIR__);
}
public function testInit()
{
$this->cleanDirs();
}
/**
* test <{ }> delimiter
@@ -17,17 +17,9 @@ class UserliteralTest extends PHPUnit_Smarty
{
public function setUp(): void
{
if (!property_exists('Smarty', 'literals')) {
$this->markTestSkipped('user literal support');
} else {
$this->setUpSmarty(__DIR__);
}
$this->setUpSmarty(__DIR__);
}
public function testInit()
{
$this->cleanDirs();
}
public function testUserLiteral()
{

Some files were not shown because too many files have changed in this diff Show More