Protecting a Theme
A theme has no module class, and its templates cannot be encrypted. What can be protected, where the licence check goes, and what a template can honestly enforce.
Overview
Only PHP can be encrypted. In a theme that is hooks.php, theme.php, and the views themselves only when the theme declares engine: 'php'.
A template prints. The value it compares against and the code that decides live in encrypted PHP.
Prerequisites
Structure
Three layers, and the first two are the same file.
| Layer | Where | What it does |
|---|---|---|
| Check | hooks.php, encrypted | Asks your server. It is included once before the first page is shown, so an unlicensed copy is judged first |
| Publish | hooks.php, encrypted | Puts a seal into the template data when the answer was yes, and nothing when it was not |
| Enforce | every protected template | Compares against the literal seal it carries. The packager encrypts nothing here, so the comparison is the whole of it |
The file is included only if it exists. A deleted hooks.php publishes no seal, and every protected template fails closed.
Walkthrough
Put the check in hooks.php
- Ask your server from
hooks.php. It runs once, before the first page is shown, which is the only such place a theme has. - Cache the answer. A theme is on every page, so a check without a cache is a request per page view.
- Decide there, in the encrypted file. What reaches the templates is a value, never a reason.
Publish the seal from one handler
- Register a single
filter:template.variableshandler for the theme and fold everything the theme needs into it. - Add the seal to the array it returns when the licence answered yes, and leave it out when it did not.
- Never register a second handler. Only the last returned array survives, so a second registration silently drops the first one's data.
Compare the value, not a flag
- Compare against the literal string the template carries. A boolean is worthless here: anyone can register a hook of their own and publish a true.
- Put the comparison in every template that must not run unlicensed, not in one shared wrapper. One check is one file to find and edit.
- Make the licensed path produce something the page needs, so removing the check breaks the design instead of hiding a warning.
Full protection means a PHP theme
- Declare
engine: 'php'in the manifest. The views become PHP and can be encrypted with the rest. - You give up the sandbox and take on escaping yourself, which is the trade the engine choice states plainly.
- Assets stay readable either way.
Reference
Example
\Hook::add('filter:template.variables', 1, function ($template, $data) {
$answer = \License::remote_check('https://vendor.example/api/licence', [
'product' => 'aurora-theme',
'domain' => \Utility::getDomain(),
]);
$body = \Utility::jdecode((string) ($answer['body'] ?? ''), true);
$ok = $answer === null || ($body['data']['valid'] ?? false) === true;
// The seal is added only on a yes. Everything else this theme publishes goes in the
// same array: a second handler would drop all of it.
if ($ok) $data['aurora_seal'] = 'a7f3c1d90e5b';
return $data;
});
{if $aurora_seal|default:'' !== 'a7f3c1d90e5b'}
{include file='partials/unlicensed.tpl'}
{else}
{* the section *}
{/if}
Pitfalls
Anyone holding a licensed copy can watch what the licensed path publishes. It is a key with the lifetime of one version. Write a new one in every release, so a value taken from an old build dies at the first update.
Only the last returned array survives. A second registration drops the seal, the menus and everything else the first one published.
Make the licensed path carry something the page needs, and report installations so you can see where copies run. Encryption alone was never the theme's protection; the licence is.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.