Apertura próxima — los autores se quedan el 100 % de su precio Hazte autor Iniciar sesión Crear cuenta
Hola, inicia sesión Cuenta y listas TuÁrea de autor

La integración, entorno por entorno

1.Tu biblioteca no es la de nadie más

El archivo que descargas se genera para un producto. La clase, el nombre del archivo y el archivo de caché llevan todos un nombre derivado de ese producto, y la dirección a la que llama se ensambla en tiempo de ejecución en vez de escribirse de una pieza. Dos de tus propios productos no comparten ni un solo identificador.

Seamos claros sobre lo que eso compra. Quien abra tus archivos y los lea encuentra la comprobación en dos minutos: eso no ha cambiado y no cambiará nunca. Lo que sí detiene es lo otro: un script que busca un nombre de clase conocido en miles de productos y lo quita solo. Así se fabrican las copias nulled en serie, y aquí ya no funciona. El coste de piratear tu producto pasa de nada a una revisión manual, producto por producto.

Los ejemplos de esta página llaman a la clase Licence. En tu descarga lleva su nombre real, mostrado junto al botón de descarga en tu área de autor.

2.La integración, entorno por entorno

Abre el que corresponda a tu producto. Cada bloque está completo y listo para pegar; renombra monmodule y adapta el almacenamiento de la clave a tus propios ajustes. Los entornos PHP usan la biblioteca descargable; los demás muestran el cliente completo, con caché y verificación de firma incluidas.

PHP PHP sin framework SDK PHP 5.6 → 8.4

La forma más corta. Pon la comprobación en la entrada de aquello que proteges: un controlador frontal, una página de administración, un script programado.

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 y 8 SDK 1.7 · 8 · 9

Mantén una sola instancia para todo el módulo. Comprueba en getContent() para que el comerciante vea por qué la pantalla de configuración se niega, y también en tus hooks de visualización para que un módulo a medio configurar no se muestre en la tienda.

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

Comprobado en 1.7, 8 y 9. Las llamadas usadas — Module, Configuration, Tools, _PS_CACHE_DIR_ — han atravesado las eliminaciones de la 9.0 sin tocarse. Las deprecaciones anunciadas para la 10.0 afectan a los controladores de administración, que este ejemplo no usa.

WP WordPress SDK 5.0 → 7.1

Una instancia estática en una función, un aviso en la administración y el cuerpo del plugin detrás de la comprobación. No hagas la comprobación en el front de cada página: la caché la hace barata, pero un aviso pertenece a la administración.

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

Las cinco llamadas usadas — add_action, add_filter, get_option, esc_html, WP_CONTENT_DIR — no han cambiado desde WordPress 5.0 y siguen funcionando en la rama 7.1. Nada de esto depende del editor de bloques ni de una ruta REST.

En WooCommerce, condiciona lo que añades a la tienda en lugar del plugin entero: una pasarela de pago que desaparece es más clara para el comerciante que un plugin que muere en silencio.

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

Envuelve la biblioteca en un modelo e inyéctalo donde lo necesites. El directorio de caché debe ser el de Magento, o un despliegue borrará tu veredicto en cada publicación.

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 biblioteca no tiene espacio de nombres: decláralas en el composer.json de tu módulo en lugar de llamar a require_once a mano.

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

Un middleware es el sitio adecuado: regístralo en el grupo de rutas que pertenece a tu producto, nunca de forma global, para que un problema de licencia nunca tumbe el resto de la aplicación.

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 suscriptor de eventos en kernel.request, que ignora las subpeticiones. Pasa la clave y el directorio de caché como argumentos en tu definición de servicio en lugar de leer la configuración dentro de la clase.

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() llegó en Symfony 5.3. En una versión anterior, llama a isMasterRequest() en su lugar: todo lo demás es idéntico.

JS Node.js API Node 18+

Ninguna dependencia más allá de la biblioteca estándar. Este es el cliente completo: respuesta firmada, caché vinculada a su host, reutilización de 24 horas y prórroga de 30 días, las mismas reglas que la biblioteca 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 o posterior: fetch pasó a ser global en la 18, y AbortSignal.timeout en la 17.3. En un runtime anterior, sustituye esos dos por https.request y tu propio temporizador.

Después llámalo una vez por petición, y nunca bloquees por un fallo de red.

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

No hay forma honesta de comprobar una licencia en un navegador. Lo que escribas se ejecuta en la máquina del comprador, en código que puede leer, y quitarlo cuesta una línea en las herramientas de desarrollo. No ofrecemos una biblioteca de front porque sería venderte un placebo.

Si tu producto tiene servidor, comprueba allí y expón el veredicto como un dato. La interfaz podrá entonces decir algo útil sin que la comprobación esté en el navegador.

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 tu producto es una plantilla de front sin ningún servidor, no hay nada que comprobar en tiempo de ejecución y no conviene fingir lo contrario. La licencia se aplica donde puede: en la descarga y mediante los términos que tu comprador aceptó.

SH Shopify API

Una app de Shopify tiene su propio servidor: usa allí el cliente Node o PHP y envía el dominio de la tienda, la dirección myshopify.com o el dominio propio si es en él en el que te apoyas. Un tema de Shopify no tiene servidor, así que se aplica la sección de front de arriba.

PY Python API Python 3.7+

Biblioteca estándar más cryptography para la firma. Las mismas reglas que en todas partes: reutilizar 24 horas, conservar el último veredicto 30 días si callamos, y bloquear solo ante un rechazo firmado.

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 en crudo API

Para todo lo demás: Go, Ruby, .NET, Java, un script de shell. Un POST, un cuerpo JSON. Los campos están en la sección de la API más abajo, y lo que debes verificar antes de fiarte de la respuesta, en la sección de la firma.

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

¿Algo no queda claro?

Escríbenos desde tu área de autor. Una pregunta que hubo que hacer suele significar que a esta página le falta un párrafo.

Área de autor