Protecting a Theme

9 views Markdown

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

No secrets and no decisions in templates

A template prints. The value it compares against and the code that decides live in encrypted PHP.

Prerequisites

A licence server of your own Where the question goes and what an answer means are yours: Licensing a Marketplace Product.
A hooks.php in the theme Optional for an ordinary theme, required for a licensed one. It is the only file that runs before the first page is shown.
Accepting that assets travel readable CSS, JavaScript and images ship as they are. They can be copied; a theme that runs on them alone cannot be installed.

Structure

Three layers, and the first two are the same file.

LayerWhereWhat it does
Checkhooks.php, encryptedAsks your server. It is included once before the first page is shown, so an unlicensed copy is judged first
Publishhooks.php, encryptedPuts a seal into the template data when the answer was yes, and nothing when it was not
Enforceevery protected templateCompares 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

  1. Ask your server from hooks.php. It runs once, before the first page is shown, which is the only such place a theme has.
  2. Cache the answer. A theme is on every page, so a check without a cache is a request per page view.
  3. Decide there, in the encrypted file. What reaches the templates is a value, never a reason.

Publish the seal from one handler

  1. Register a single filter:template.variables handler for the theme and fold everything the theme needs into it.
  2. Add the seal to the array it returns when the licence answered yes, and leave it out when it did not.
  3. 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

  1. 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.
  2. 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.
  3. 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

  1. Declare engine: 'php' in the manifest. The views become PHP and can be encrypted with the rest.
  2. You give up the sandbox and take on escaping yourself, which is the trade the engine choice states plainly.
  3. Assets stay readable either way.

Reference

hooks.phpEncryptable. The check, the cache and the publish live here.
theme.phpEncryptable. Manifest: engine, meta and the settings schema.
views, engine phpEncryptable, because they are PHP.
views, tpl or twigNot encryptable. Compiled to plain PHP on disk before any check can run.
assetsNot encryptable. CSS, JavaScript, fonts and images ship as they are.

Example

hooks.php, one handler, encrypted
\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;
});
a protected template
{if $aurora_seal|default:'' !== 'a7f3c1d90e5b'}
    {include file='partials/unlicensed.tpl'}
{else}
    {* the section *}
{/if}

Pitfalls

The seal is not a secret

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.

One handler per theme

Only the last returned array survives. A second registration drops the seal, the menus and everything else the first one published.

Weight belongs on the two real defences

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.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.