The file you download is generated for one product. The class, the file name and the cache file all carry a name derived from that product, and the address it calls is assembled at runtime rather than written as one string. Two of your own products do not share a single identifier.
Be clear about what that buys. Anyone who opens your files and reads them finds the check in a couple of minutes — that has not changed and never will. What it stops is the other thing: a script that greps thousands of products for one known class name and strips it automatically. That is how nulled copies are produced at scale, and it no longer works here. The cost of pirating your product goes from nothing to a manual look, per product.
The examples on this page call the class Licence. In your own download it has the real name, shown next to the download button in your author area.
Open the one that matches your product. Each block is complete and ready to paste; rename monmodule and adapt the storage of the key to your own settings. PHP stacks use the downloadable library; the others show the full client, cache and signature check included.
The shortest form. Put the check at the entry point of whatever you are protecting — a front controller, an admin page, a scheduled script.
require_once __DIR__ . '/Licence.php';
$licence = new Licence(
$config['licence'],
__DIR__ . '/cache'
);
if (!$licence->valide()) {
http_response_code(403);
exit($licence->message());
}
Keep one instance for the whole module. Check in getContent() so the merchant sees why the configuration screen refuses, and again in your display hooks so a module left half-configured does not render on the storefront.
require_once dirname(__FILE__) . '/Licence.php';
class MonModule extends Module
{
private $licence = null;
private function licence()
{
if ($this->licence === null) {
$this->licence = new Licence(
Configuration::get('MONMODULE_LICENCE'),
_PS_CACHE_DIR_ . 'monmodule'
);
}
return $this->licence;
}
public function getContent()
{
if (Tools::isSubmit('submitLicence')) {
Configuration::updateValue('MONMODULE_LICENCE', Tools::getValue('licence'));
}
$l = $this->licence();
if (!$l->valide()) {
return $this->displayError($l->message()) . $this->formulaireLicence();
}
if ($l->essai()) {
return $this->displayWarning($this->l('Development licence')) . $this->contenu();
}
return $this->contenu();
}
public function hookDisplayHeader()
{
if (!$this->licence()->valide()) {
return '';
}
return $this->contenuFront();
}
}
Checked against 1.7, 8 and 9. The calls used — Module, Configuration, Tools, _PS_CACHE_DIR_ — survived the 9.0 removals untouched. The deprecations announced for 10.0 concern the admin controllers, which this snippet does not use.
A static instance in one function, an admin notice, and the plugin body behind the check. Do not run the check on the front end of every page — the cache makes it cheap, but a notice belongs in the admin.
require_once __DIR__ . '/Licence.php';
function monplug_licence() {
static $l = null;
if ($l === null) {
$l = new Licence(
get_option('monplug_licence'),
WP_CONTENT_DIR . '/cache/monplug'
);
}
return $l;
}
add_action('admin_notices', function () {
$l = monplug_licence();
if (!$l->valide()) {
printf('<div class="notice notice-error"><p>%s</p></div>', esc_html($l->message()));
} elseif ($l->essai()) {
echo '<div class="notice notice-warning"><p>'
. esc_html__('Development licence', 'monplug') . '</p></div>';
}
});
add_action('init', function () {
if (monplug_licence()->valide()) {
monplug_demarrer();
}
});
The five calls used — add_action, add_filter, get_option, esc_html, WP_CONTENT_DIR — have not changed since WordPress 5.0 and still work on the 7.1 branch. Nothing here depends on the block editor or on a REST route.
For WooCommerce, gate what you add to the shop rather than the plugin as a whole: a payment gateway that disappears is clearer to the merchant than a plugin that dies silently.
add_filter('woocommerce_payment_gateways', function ($passerelles) {
if (monplug_licence()->valide()) {
$passerelles[] = 'WC_Gateway_MonPlug';
}
return $passerelles;
});
add_action('woocommerce_admin_field_monplug_licence', function () {
$l = monplug_licence();
if (!$l->valide()) {
echo '<div class="error inline"><p>' . esc_html($l->message()) . '</p></div>';
}
});
Wrap the library in a model and inject it where you need it. The cache directory must be the one Magento owns, or a deployment will wipe your verdict on every release.
namespace Editeur\MonModule\Model;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Filesystem\DirectoryList;
class Licence
{
private $licence;
public function __construct(ScopeConfigInterface $config, DirectoryList $dossiers)
{
$this->licence = new \Licence(
(string) $config->getValue('monmodule/general/licence'),
$dossiers->getPath(DirectoryList::CACHE) . '/monmodule'
);
}
public function valide(): bool
{
return $this->licence->valide();
}
public function message(): string
{
return $this->licence->message();
}
}
The library has no namespace, so declare it in your module’s composer.json rather than calling require_once by hand.
{
"autoload": {
"files": [
"Licence.php"
]
}
}
A middleware is the right place: register it on the route group your product owns, never globally, so a licence problem never takes down the rest of the application.
namespace App\Licence;
use Closure;
use Illuminate\Http\Request;
class VerifieLicence
{
private $licence;
public function __construct()
{
$this->licence = new \Licence(
config('monmodule.licence'),
storage_path('app/monmodule')
);
}
public function handle(Request $requete, Closure $suivant)
{
if (!$this->licence->valide()) {
abort(403, $this->licence->message());
}
return $suivant($requete);
}
}
An event subscriber on kernel.request, skipping sub-requests. Pass the key and the cache directory as arguments in your service definition rather than reading configuration inside the class.
namespace App\Licence;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
class LicenceSubscriber implements EventSubscriberInterface
{
private $licence;
public function __construct(string $cle, string $dossierCache)
{
$this->licence = new \Licence($cle, $dossierCache);
}
public function onKernelRequest(RequestEvent $evenement): void
{
if (!$evenement->isMainRequest()) {
return;
}
if (!$this->licence->valide()) {
throw new AccessDeniedHttpException($this->licence->message());
}
}
public static function getSubscribedEvents(): array
{
return [KernelEvents::REQUEST => ['onKernelRequest', 16]];
}
}
isMainRequest() arrived in Symfony 5.3. On an older version, call isMasterRequest() instead — everything else is identical.
No dependency beyond the standard library. This is the complete client: signed response, cache bound to its host, 24-hour reuse and 30-day grace period — the same rules as the PHP library.
import { createPublicKey, verify } from 'node:crypto';
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
const API = 'https://addonsmarket.com/api/licence';
const PUBLIQUE = '/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw=';
const SURSIS = 2592000;
const publique = createPublicKey({
key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'),
Buffer.from(PUBLIQUE, 'base64')]),
format: 'der',
type: 'spki',
});
const canonique = (d) =>
JSON.stringify(Object.fromEntries(Object.keys(d).sort().map((k) => [k, d[k]])));
const signee = (rep, hote) =>
rep && rep.donnees && rep.signature
&& rep.donnees.hote === hote
&& verify(null, Buffer.from(canonique(rep.donnees), 'utf8'), publique,
Buffer.from(rep.signature, 'base64'));
export async function etat(cle, hote, fichierCache) {
const maintenant = Math.floor(Date.now() / 1000);
let cache = null;
try {
const lu = JSON.parse(await readFile(fichierCache, 'utf8'));
if (signee(lu, hote)) cache = lu;
} catch {}
if (cache && cache.donnees.revalider > maintenant) return cache.donnees;
try {
const r = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'User-Agent': 'MonProduit/1.0' },
body: JSON.stringify({ cle, url: hote }),
signal: AbortSignal.timeout(6000),
});
if (!r.ok) throw new Error(String(r.status));
const rep = await r.json();
if (!signee(rep, hote)) throw new Error('signature');
await mkdir(dirname(fichierCache), { recursive: true });
await writeFile(fichierCache, JSON.stringify(rep));
return rep.donnees;
} catch {
if (cache && cache.donnees.emis + SURSIS > maintenant) return cache.donnees;
return { ok: false, motif: 'reseau' };
}
}
Node 18 or later: fetch became global in 18, and AbortSignal.timeout in 17.3. On an older runtime, replace those two with https.request and your own timer.
Then call it once per request, and never block on a network failure.
import { etat } from './licence.mjs';
const d = await etat(process.env.LICENCE, req.hostname, './var/licence.json');
if (!d.ok && d.motif !== 'reseau') {
return res.status(403).json({ erreur: d.motif });
}
There is no honest way to check a licence in a browser. Whatever you write runs on the buyer’s machine, in code they can read, and removing it takes one line in the devtools. We do not ship a front-end library because we would be selling you a placebo.
If your product has a backend, check there and expose the verdict as data. The interface can then say something useful without the check itself being in the browser.
app.get('/api/etat', async (req, res) => {
const d = await etat(process.env.LICENCE, req.hostname, './var/licence.json');
res.json({ actif: d.ok, essai: d.essai });
});
const [actif, setActif] = useState(null);
useEffect(() => {
fetch('/api/etat')
.then((r) => r.json())
.then((d) => setActif(d.actif));
}, []);
if (actif === false) {
return <Bandeau>Licence inactive</Bandeau>;
}
If your product is a pure front-end template with no backend at all, there is nothing to check at runtime and you should not pretend otherwise. The licence is enforced where it can be: at download, and by the terms your buyer accepted.
A Shopify app has a backend of its own: use the Node or PHP client there, and send the shop domain — the myshopify.com address, or the custom domain if that is what you key on. A Shopify theme has no backend, so the front-end section above applies instead.
Standard library plus cryptography for the signature. Same rules as everywhere else: reuse for 24 hours, keep the last verdict for 30 days if we go quiet, block only on a signed refusal.
import base64, json, time, urllib.request
from pathlib import Path
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
API = 'https://addonsmarket.com/api/licence'
PUBLIQUE = '/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw='
SURSIS = 2592000
_publique = Ed25519PublicKey.from_public_bytes(base64.b64decode(PUBLIQUE))
def _canonique(d):
return json.dumps(d, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
def _signee(rep, hote):
try:
if rep['donnees']['hote'] != hote:
return False
_publique.verify(base64.b64decode(rep['signature']),
_canonique(rep['donnees']).encode('utf-8'))
return True
except (KeyError, TypeError, ValueError, InvalidSignature):
return False
def etat(cle, hote, fichier_cache):
maintenant = int(time.time())
chemin = Path(fichier_cache)
cache = None
try:
lu = json.loads(chemin.read_text(encoding='utf-8'))
if _signee(lu, hote):
cache = lu
except (OSError, ValueError):
pass
if cache and cache['donnees']['revalider'] > maintenant:
return cache['donnees']
try:
corps = json.dumps({'cle': cle, 'url': hote}).encode('utf-8')
requete = urllib.request.Request(API, data=corps, headers={
'Content-Type': 'application/json',
'User-Agent': 'MonProduit/1.0',
})
with urllib.request.urlopen(requete, timeout=6) as r:
rep = json.loads(r.read().decode('utf-8'))
if not _signee(rep, hote):
raise ValueError('signature')
chemin.parent.mkdir(parents=True, exist_ok=True)
chemin.write_text(json.dumps(rep), encoding='utf-8')
return rep['donnees']
except Exception:
if cache and cache['donnees']['emis'] + SURSIS > maintenant:
return cache['donnees']
return {'ok': False, 'motif': 'reseau'}
For anything else — Go, Ruby, .NET, Java, a shell script. One POST, one JSON body. Read the API section below for the fields, and the signature section for what you must verify before trusting the answer.
curl -X POST https://addonsmarket.com/api/licence \
-H 'Content-Type: application/json' \
-H 'User-Agent: MonProduit/1.0' \
-d '{"cle":"VOTRE_CLE","url":"boutique.exemple.fr"}'
Write to us from your author area. A question that needed asking usually means this page is missing a paragraph.