v2.0.1 — Single-File PHP Template Engine with Apache SSI, html4.pl Compatibility & Hardened Security
LatestHTML.PHP 2.0.1 is a production-ready single-file PHP template engine for PHP 8.3+ with native Apache SSI directive support, an html4.pl-compatible legacy API, XSS-safe variable substitution and zero dependencies — no Composer, no autoloader, no build step, nothing beyond the PHP standard library.
Drop one file into any project and render templates that mix plain HTML with #include, #set, #echo, %{VARIABLE} substitution, numeric formatting directives and html4.pl-style blocks. Built for shared hosting, legacy sites and modern PHP 8 applications alike.
Keywords: PHP template engine · PHP 8.3 template library · Apache SSI / server-side includes · html4.pl compatibility · single-file PHP library · Composer-free · drop-in replacement · XSS protection · path-traversal safe includes
🌟 Highlights
- 📦 Single-file distribution — one
require, no Composer, no autoloader, no build step. Ideal for shared hosting where/var/www/lib/html.phpis shared by many virtual hosts. - 🔌 Apache SSI compatibility —
#config,#set,#include virtual,#echoand the legacy##echo, evaluated in the same order as Apache. Attribute order is irrelevant; single and double quotes both work. - 🏛️ html4.pl parity —
LoadTemplate(),LoadVar(),Read_HTML(),Show_HTML(),Divide(),lc_Tags(),normalize(),clean(),clean_html(),WriteFile(),TrimSpaces(). - 🧩 Rich substitution syntax —
%{NAME},#{NAME},%[NAME], translation lookups with@{KEY}, and numeric directives:%{AMT.Round:2},%{AMT.Clean Round:2},%{AMT.Div 4},%{AMT.Round Div 4:2}. - 🛡️ Hardened by design — escaping by default, reserved-name protection for every superglobal and config key, bounded recursion, include containment and atomic file writes.
- 🧪 Verifiable — a zero-dependency suite of 276 tests / 748 assertions across 35 groups, all passing on PHP 8.5.
🚀 Quick Start
<?php
require __DIR__ . '/html.php';
$GLOBALS['Title'] = 'Dashboard';
$GLOBALS['User'] = 'mikhail';
$GLOBALS['Amount'] = 1234.5678;
echo HTML('<h1>%{Title}</h1><p>Welcome, %{User}!</p><td>%{Amount.Round:2}</td>');
// <h1>Dashboard</h1><p>Welcome, mikhail!</p><td>1234.57</td>Render a page with SSI partials:
<!-- www/page.htm -->
<!--#config timefmt="%Y-%m-%d" -->
<!--#set var="PageTitle" value="Reports" -->
<!--#include virtual="/ssi/head.htm" -->
<h1><!--#echo var="PageTitle" --></h1>
<p>Generated <!--#echo var="DATE_LOCAL" --></p>$GLOBALS['HTML_WWW_PATH'] = __DIR__ . '/www';
echo HTML(file_get_contents(__DIR__ . '/www/page.htm'));🛡️ Security Hardening in 2.0.1
| Threat | Mitigation shipped in this release |
|---|---|
| Output injection through SSI echo | HTML_SSI_ECHO_ESCAPE is now true by default — #echo output is HTML-escaped |
Overwriting $_SESSION, $_SERVER or configuration globals |
HTML_RESERVED_GLOBALS enforced on every directive-driven read and write |
Hostile variable names (null bytes, ../, spaces, leading digits) |
New _htmlValidVariableName() gate on every $GLOBALS access |
Reading arbitrary globals through #echo |
Optional HTML_SSI_ECHO_ALLOW read allow-list |
Path traversal through #include |
.. rejected unless HTML_SSI_ALLOW_PARENT; resolved path must stay inside the document root; case-insensitive comparison on Windows/macOS |
| Include / recursion denial of service | Depth, tag-count, template-size and include-size caps plus include-stack cycle detection |
| Silent partial writes | WriteFile() verifies the complete payload length (temp file + rename, locked fallback) |
| Superglobal corruption | HTML_AUTO_SANITIZE_INPUT is now false; sanitised data is returned by value from the new HTML_Sanitized_Input() |
🧰 Correctness Fixes and Robustness
- SSI time formats fixed — literal text is escaped, so
Updated at %H:%Mrenders asUpdated at 14:30instead of leakingdate()tokens.%hnow maps to the abbreviated month (Sep), matchingstrftime. - Flexible directive parsing — attributes are parsed generically, so
<!--#set value="VAL" var="NAME" -->works as well as the canonical order, while quoted values containing>still parse. normalize()keeps its<br>marker — line breaks survive normalisation, and a forged marker in the input can no longer fabricate a<br>.Read_HTML()respects published blocks — a block loaded from an<!--@block N-->marker is no longer overwritten by the file's residual text.clean_html()drops code elements —<script>and<style>are removed together with their contents (including an unterminated opener); every other removed tag keeps its inner text.- Quiet file writes —
WriteFile()no longer emits a PHP warning from its locked in-place fallback; failures are still reported asRuntimeException. Show_HTML()accepts block'0'— a numeric block of0is no longer treated as "missing".
📦 Installation
Option 1 — single file (recommended)
curl -L -o html.php https://github.com/paulmann/HTML.PHP/releases/download/v2.0.1/html.phprequire __DIR__ . '/html.php';Option 2 — clone the project (library + tests + examples)
git clone https://github.com/paulmann/HTML.PHP.git
cd HTML.PHP
php tests/run-tests.php
php examples/01-quick-start.phpOption 3 — shared hosting
scp html.php user@host:/var/www/lib/html.php🧪 Verification
php -l html.php # No syntax errors detected
php tests/run-tests.php # SUMMARY: 276 passed, 0 failed (748 assertions)
php tests/run-tests.php --filter=security
php tests/run-tests.php --verbose========================================================================
SUMMARY: 276 passed, 0 failed (748 assertion(s))
The suite is fully dependency-free (no PHPUnit, no Composer), enforces $GLOBALS/superglobal isolation around every test, boots a separate PHP process as a clean-interpreter meta-test, and is independent of the working directory and machine timezone.
🔁 Upgrading from an earlier build
The public API is unchanged — same function names, same signatures, same return types. Two defaults changed deliberately:
| Behaviour | Before | In 2.0.1 | Restore the old behaviour with |
|---|---|---|---|
#echo output |
emitted raw | HTML-escaped | $GLOBALS['HTML_SSI_ECHO_ESCAPE'] = false; before the include |
| Superglobals on include | rewritten by sanitizeXSS() |
untouched | HTML_Sanitized_Input(), or $GLOBALS['HTML_AUTO_SANITIZE_INPUT'] = true; before the include |
Two narrow edge cases are worth an audit before upgrading an existing site:
- Variable and block names must now match
^[A-Za-z_][A-Za-z0-9_.-]{0,127}$. Names with spaces, leading digits or non-ASCII letters are ignored instead of published to$GLOBALS. %hin an SSI time format now yieldsSepinstead ofSeptember.
Full details: Backward Compatibility & Migration.
📖 Documentation
- README — features, API reference, SSI directive reference, configuration, security model
- CHANGELOG — full version history
- Examples — four runnable scripts
- Test suite — 276 tests across 9 case files
📄 License
MIT — see LICENSE.
Full Changelog: https://github.com/paulmann/HTML.PHP/commits/v2.0.1
If this release saved you a Composer dependency, a ⭐ on the repository helps others find it.