Skip to content

v2.0.1 — Single-File PHP Template Engine with Apache SSI, html4.pl Compatibility & Hardened Security

Latest

Choose a tag to compare

@paulmann paulmann released this 20 Sep 19:22
· 1 commit to main since this release

HTML.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.php is shared by many virtual hosts.
  • 🔌 Apache SSI compatibility — #config, #set, #include virtual, #echo and 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:%M renders as Updated at 14:30 instead of leaking date() tokens. %h now maps to the abbreviated month (Sep), matching strftime.
  • 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 as RuntimeException.
  • Show_HTML() accepts block '0' — a numeric block of 0 is 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.php
require __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.php

Option 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.
  • %h in an SSI time format now yields Sep instead of September.

Full details: Backward Compatibility & Migration.


📖 Documentation

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