Ouverture prochaine — les auteurs gardent 100 % de leur prix Devenir auteur Se connecter Créer un compte
Bonjour, identifiez-vous Compte et listes EspaceAuteur

L’intégration, environnement par environnement

1.Votre bibliothèque n’est celle de personne d’autre

Le fichier que vous téléchargez est généré pour un produit. La classe, le nom de fichier et le fichier de cache portent tous un nom dérivé de ce produit, et l’adresse appelée est assemblée à l’exécution plutôt qu’écrite d’un bloc. Deux de vos propres produits ne partagent pas un seul identifiant.

Soyons clairs sur ce que cela vous achète. Qui ouvre vos fichiers et les lit trouve le contrôle en deux minutes — cela n’a pas changé et ne changera jamais. Ce que cela arrête, c’est l’autre chose : un script qui cherche un nom de classe connu dans des milliers de produits et le retire tout seul. C’est ainsi que les copies nullées se fabriquent en série, et cela ne marche plus ici. Le coût du piratage de votre produit passe de rien à un examen manuel, produit par produit.

Les exemples de cette page appellent la classe Licence. Dans votre téléchargement, elle porte son vrai nom, affiché à côté du bouton de téléchargement dans votre espace auteur.

2.L’intégration, environnement par environnement

Ouvrez celui qui correspond à votre produit. Chaque bloc est complet et prêt à coller ; renommez monmodule et adaptez le rangement de la clé à vos propres réglages. Les environnements PHP utilisent la bibliothèque à télécharger ; les autres montrent le client complet, cache et vérification de signature compris.

PHP PHP sans cadriciel SDK PHP 5.6 → 8.4

La forme la plus courte. Placez le contrôle à l’entrée de ce que vous protégez — un contrôleur frontal, une page d’administration, un script planifié.

myproduct.php
require_once __DIR__ . '/Licence.php';

$licence = new Licence(
    $config['licence'],
    __DIR__ . '/cache'
);

if (!$licence->valide()) {
    http_response_code(403);
    exit($licence->message());
}
PS PrestaShop 1.7 et 8 SDK 1.7 · 8 · 9

Gardez une seule instance pour tout le module. Contrôlez dans getContent() pour que le marchand voie pourquoi l’écran de configuration refuse, et dans vos hooks d’affichage pour qu’un module resté à moitié configuré ne s’affiche pas en boutique.

mymodule.php
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();
    }
}

Vérifié sur 1.7, 8 et 9. Les appels utilisés — Module, Configuration, Tools, _PS_CACHE_DIR_ — ont traversé les suppressions de la 9.0 sans être touchés. Les dépréciations annoncées pour la 10.0 concernent les contrôleurs d’administration, que cet exemple n’utilise pas.

WP WordPress SDK 5.0 → 7.1

Une instance statique dans une fonction, un avis dans l’administration, et le corps de l’extension derrière le contrôle. Ne faites pas le contrôle sur le front de chaque page — le cache le rend peu coûteux, mais un avis a sa place dans l’administration.

myplugin.php
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();
    }
});

Les cinq appels utilisés — add_action, add_filter, get_option, esc_html, WP_CONTENT_DIR — n’ont pas bougé depuis WordPress 5.0 et fonctionnent toujours sur la branche 7.1. Rien ici ne dépend de l’éditeur de blocs ni d’une route REST.

Pour WooCommerce, conditionnez ce que vous ajoutez à la boutique plutôt que l’extension entière : une passerelle de paiement qui disparaît est plus claire pour le marchand qu’une extension qui meurt en silence.

woocommerce.php
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>';
    }
});
M2 Magento 2 SDK 2.4

Enveloppez la bibliothèque dans un modèle et injectez-la où vous en avez besoin. Le dossier de cache doit être celui de Magento, sinon un déploiement effacera votre verdict à chaque mise en production.

Model/License.php
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();
    }
}

La bibliothèque n’a pas d’espace de noms : déclarez-la dans le composer.json de votre module plutôt que d’appeler require_once à la main.

composer.json
{
  "autoload": {
    "files": [
      "Licence.php"
    ]
  }
}
LV Laravel SDK 9 → 12

Un middleware est la bonne place : enregistrez-le sur le groupe de routes qui appartient à votre produit, jamais globalement, pour qu’un souci de licence n’emporte jamais le reste de l’application.

app/License/CheckLicense.php
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);
    }
}
SF Symfony SDK 5.3 → 7

Un abonné d’événement sur kernel.request, qui ignore les sous-requêtes. Passez la clé et le dossier de cache en arguments dans votre définition de service plutôt que de lire la configuration dans la classe.

src/License/LicenseSubscriber.php
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() est arrivée dans Symfony 5.3. Sur une version antérieure, appelez isMasterRequest() à la place — tout le reste est identique.

JS Node.js API Node 18+

Aucune dépendance au-delà de la bibliothèque standard. Voici le client complet : réponse signée, cache lié à son hôte, réutilisation 24 heures et sursis de 30 jours — les mêmes règles que la bibliothèque PHP.

license.mjs
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 ou plus récent : fetch est devenu global en 18, et AbortSignal.timeout en 17.3. Sur une version antérieure, remplacez ces deux-là par https.request et votre propre minuteur.

Appelez-le ensuite une fois par requête, et ne bloquez jamais sur une panne réseau.

server.mjs
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 });
}
UI React, Vue, Angular API

Il n’existe pas de façon honnête de vérifier une licence dans un navigateur. Ce que vous écrivez tourne sur la machine de l’acheteur, dans du code qu’il peut lire, et le retirer prend une ligne dans les outils de développement. Nous ne fournissons pas de bibliothèque front parce que ce serait vous vendre un placebo.

Si votre produit a un serveur, contrôlez là-bas et exposez le verdict comme une donnée. L’interface peut alors dire quelque chose d’utile sans que le contrôle lui-même soit dans le navigateur.

server.mjs
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 });
});
License.jsx
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>;
}

Si votre produit est un gabarit front sans aucun serveur, il n’y a rien à contrôler à l’exécution et il ne faut pas faire semblant. La licence s’applique là où elle le peut : au téléchargement, et par les conditions que votre acheteur a acceptées.

SH Shopify API

Une application Shopify a son propre serveur : utilisez-y le client Node ou PHP, et envoyez le domaine de la boutique — l’adresse myshopify.com, ou le domaine personnalisé si c’est sur lui que vous vous appuyez. Un thème Shopify n’a pas de serveur : c’est la section front ci-dessus qui s’applique.

PY Python API Python 3.7+

Bibliothèque standard, plus cryptography pour la signature. Mêmes règles qu’ailleurs : réutilisation 24 heures, conservation du dernier verdict 30 jours si nous nous taisons, blocage seulement sur un refus signé.

license.py
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'}
{ } HTTP brut API

Pour tout le reste — Go, Ruby, .NET, Java, un script shell. Un POST, un corps JSON. Voyez la section API plus bas pour les champs, et la section signature pour ce que vous devez vérifier avant de faire confiance à la réponse.

curl
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"}'

Quelque chose n’est pas clair ?

Écrivez-nous depuis votre espace auteur. Une question qu’il a fallu poser signifie en général qu’il manque un paragraphe à cette page.

Auteur