Search by

licenceo / wordpress-sdk

licenceo

SDK d'intégration des licences Licenceo pour plugins et thèmes WordPress (activation, revalidation, mises à jour, remontée de plantage).

v1.4.3 2026-09-29 21:21 UTC

This package is auto-updated.

Last update: 2026-09-29 19:21:46 UTC


README

SDK d'intégration des licences Licenceo pour plugins et thèmes WordPress. Il gère l'activation, la revalidation périodique, les mises à jour automatiques et la remontée de plantage — vous n'avez rien d'autre à écrire côté client.

Ce dépôt n'est pas un plugin autonome : vous l'intégrez dans votre plugin ou thème, puis vous initialisez le SDK avec la configuration de votre produit.

Installation

Composer

Depuis le dossier de votre plugin ou thème :

composer require licenceo/wordpress-sdk

Le SDK est distribué sur Packagist, depuis le dépôt public GitLab.

Dans le fichier principal du plugin, ou functions.php du thème, après le chargement de WordPress :

require_once __DIR__ . '/vendor/autoload.php';
$licence = \Licenceo\SDK\SDK::init([
    'api_url'     => 'https://api.licenceo.app/api',
    'product_id'  => 12, // votre produit Licenceo
    'slug'        => 'mon-super-plugin',
    'plugin_file' => __FILE__, // fichier principal du plugin
    'version'     => '2.3.0', // version installée de VOTRE plugin, pas celle du SDK
]);
// Le SDK enregistre lui-même le hook WordPress de désactivation du plugin.
// Il nettoie le cron, sans libérer la licence. Voir le parcours et les actions ci-dessous.

Conservez composer.json et composer.lock dans votre projet. Embarquez vendor/ dans le ZIP livré aux clients (composer install --no-dev --prefer-dist --optimize-autoloader pendant la préparation du ZIP). Vos clients n’ont pas besoin de Composer.

Le SDK nécessite WordPress pour fonctionner ; charger l’autoloader dans un outil CLI hors WordPress ne doit pas arrêter cet outil. La règle de cohabitation s'applique aussi avec Composer : si plusieurs plugins embarquent le SDK, la copie la plus récente fournit les classes (voir « Plusieurs produits basés sur ce SDK sur le même site »).

Autoload et namespace

Composer déclare le préfixe PSR-4 Licenceo\SDK\ vers src/, en conservant licenceo-sdk.php dans autoload.files pour le chargement WordPress et la protection contre les copies multiples. Le namespace conserve sa casse historique SDK ; utilisez cette même casse dans vos imports.

La façade \Licenceo\SDK\SDK suffit pour le parcours standard ; les classes internes ne sont pas nécessaires.

Dans WordPress, le bootstrap charge ensemble les classes de la copie retenue : PSR-4 ne remplace pas la règle de cohabitation et ne rend pas le SDK indépendant de WordPress. Spécification PSR-4.

Deux secrets, deux moments

La clé de licence ne sert qu'une fois, à l'activation. Le SDK reçoit alors un jeton propre à ce site, le conserve, et oublie la clé.

Clé de licence
    → activation initiale
    → le serveur émet un jeton propre à cette activation
    → le SDK stocke le jeton, la clé n'est plus conservée
    → toutes les opérations suivantes utilisent le jeton

Ce que le SDK garde dans wp_options, préfixé par le slug de votre produit (donc sans collision entre extensions) :

OptionContenu
<prefix>_activation_tokenLe jeton (lat_…). Jamais affiché, jamais inclus dans une notice.
<prefix>_license_key_hintLes 4 derniers caractères de la clé, pour l'affichage seul.
<prefix>_license_statusDernier verdict connu (validité, raison, paliers). Aucun secret.

Toutes ces options sont écrites avec autoload = no (troisième argument d'update_option()). WordPress charge à chaque requête l'ensemble des options autoloadées et les conserve en cache objet : un secret n'a rien à y faire, il doit être lu explicitement au moment où on s'en sert. C'est vérifié par un test (test_no_option_written_by_the_sdk_is_autoloaded), parce qu'omettre ce troisième argument ne provoque aucune erreur visible.

Le jeton ne sort de wp_options que dans un en-tête HTTP X-Activation-Token. Il n'apparaît jamais dans une URL, une notice d'administration, un message d'erreur, ni dans l'onglet Santé du site — le SDK n'expose aucune route REST et ne s'enregistre dans aucun rapport de débogage WordPress.

Ce que ça change en cas de compromission du site. Un attaquant qui lit la base WordPress obtient un jeton qui ne vaut que pour cette activation : il ne peut pas créer d'autres activations, ni utiliser les emplacements restants de la licence, ni toucher aux autres sites. Sa révocation n'affecte qu'eux.

Ce que ça ne change pas. Une clé de licence volée reste utilisable pour créer une nouvelle activation si un emplacement est libre. Le jeton réduit la portée d'un secret ; ce n'est pas une preuve d'achat ni de possession du domaine.

Cache des mises à jour

La réponse de latest-version reste en cache pendant 12 heures. Sur cache froid, un verrou interne <prefix>_remote_version_lock, non autoloadé, limite la recherche à un seul propriétaire par site et par intégration. Les autres processus quittent le contrôle sans requête HTTP supplémentaire ; ils pourront utiliser le cache lors d'un contrôle ultérieur.

Le verrou utilise directement la table d'options du site, indépendamment du cache objet persistant : INSERT IGNORE s'appuie sur l'unicité de option_name, puis un UPDATE conditionnel permet de reprendre uniquement un bail expiré. Il n'utilise pas le couple lecture/écriture de transients, ni l'upsert d'add_option(). Après acquisition, le cache est relu en évitant un éventuel résultat négatif conservé dans le processus courant.

Le bail expire après 120 secondes, mesurées par MySQL, avec une marge sur le timeout HTTP de 15 secondes. Un processus interrompu laisse donc un verrou récupérable au prochain contrôle après expiration. La libération dans finally vérifie le propriétaire : un ancien processus ne peut pas supprimer le bail de son successeur. Une erreur HTTP, réseau ou de réponse ne remplit pas le cache et libère immédiatement le verrou. Si la base refuse l'acquisition, aucun appel API n'est tenté sans verrou.

Comme tout verrou à bail, l'exclusion vaut pendant sa durée : un processus suspendu plus de 120 secondes peut chevaucher son successeur après sa reprise. Le SDK ne prolonge pas le bail et n'ajoute ni attente active ni appel réseau pour cela. En multisite, le verrou suit la table d'options du site, comme le transient existant.

Perdre ou renouveler un jeton

Aucun endpoint ne permet de relire un jeton existant — c'est précisément ce qui empêche quiconque détient la clé de reprendre la main sur un site en place.

  • Le site a encore son jeton : $licence->rotate_token() en obtient un neuf. Opération explicite ; le SDK ne tourne jamais de lui-même. L'ancien jeton reste accepté par Licenceo jusqu'à la première utilisation du nouveau, 24 heures au plus : si la réponse se perd (délai, erreur réseau ou serveur), le site garde un jeton valide, et le SDK (1.4.3 et plus) relance la rotation à la revalidation horaire. Deux rotations ne tournent jamais en même temps sur un site.
  • Le jeton est perdu (base restaurée, site réinstallé) : l'éditeur révoque l'activation depuis son tableau de bord, ce qui libère l'emplacement, puis le site se réactive avec la clé de licence.

deactivate() efface l'état local uniquement après 200 deactivated: true ou 404 confirmant invalid_token/not_activated. Toute panne réseau, réponse 429/5xx, erreur 422 ou réponse incomplète conserve le jeton et retourne success: false. Une licence expirée ou un compte suspendu peut libérer son emplacement si le jeton est encore authentifié.

Côté serveur, trois événements invalident un jeton immédiatement : la désactivation par le site lui-même, la révocation de son activation par l'éditeur, et la révocation de la licence entière, qui coupe d'un coup toutes ses activations, dans une seule transaction.

Les tests de ce dépôt simulent WordPress et les réponses HTTP. Les tests de contrat avec le SaaS sont maintenus séparément dans le dépôt privé de l’application.

Réactiver un domaine déjà actif échoue volontairement (already_activated) et ne réémet aucun jeton.

Utilisation — Plugin

$licence = \Licenceo\SDK\SDK::init([
    'api_url'     => 'https://api.licenceo.app/api',
    'product_id'  => 12,
    'slug'        => 'mon-super-plugin',
    'plugin_file' => __FILE__,                   // fichier principal du plugin
    'version'     => '2.3.0',                    // version installée de votre plugin (pas celle du SDK)
    // Optionnel :
    // 'option_prefix'   => 'mon_super_plugin',
    // 'homepage'        => 'https://exemple.com/mon-super-plugin',
    // 'report_crashes'  => true, // activé par défaut ; false pour désactiver (voir ci-dessous).
    // 'max_offline_days' => 30,   // jours sans vérification réussie tolérés ; 0 = illimité.
]);

// Activer depuis un écran de réglages, une seule fois :
// $licence->activate($cle_saisie);   // la clé n'est pas conservée
// $licence->deactivate();            // libère ce site
// $licence->rotate_token();          // renouvelle le jeton de ce site

// Pour l'affichage : « clé se terminant par …8HJ6 »
// $licence->license_key_hint();

if ($licence->is_valid()) {
    // ... fonctionnalités premium ...
}

// Paliers (Basic/Pro/Agency) du même produit, gérés depuis le dashboard
// Licenceo — pas besoin d'un produit séparé par palier :
if ($licence->has_feature('rapports-avances')) {
    // ...
}

Remontée des plantages, active par défaut. Quand une erreur fatale PHP survient dans un fichier de votre plugin (ou thème), le SDK envoie à Licenceo le message d'erreur (500 caractères au plus), le chemin relatif du fichier et la ligne. Le message est nettoyé du chemin du serveur (même pour un plugin relié par lien symbolique, depuis 1.4.3), des adresses e-mail et des secrets reconnaissables (jetons, clés sk/pk, JWT, « password=… ») avant l'envoi. Rien d'autre n'est collecté intentionnellement, mais un message d'erreur peut encore contenir une autre donnée que votre code y a placée. Depuis la 1.4.2, la remontée fonctionne aussi avec le gestionnaire d'erreurs fatales de WordPress (actif par défaut depuis WordPress 5.2), qui empêchait les versions précédentes d'envoyer le rapport. Ces rapports servent à mettre en pause un déploiement progressif défaillant. Mentionnez-les dans votre propre politique de confidentialité, ou désactivez-les avec 'report_crashes' => false.

Écran de saisie de la clé

Le SDK ne fournit pas d'interface : c'est votre plugin qui présente le champ. Quelques règles à respecter, parce que la clé transite par ce formulaire.

add_action('admin_post_mon_plugin_activer', function () use ($licence) {
    // 1. Réservé aux administrateurs du site.
    if (! current_user_can('manage_options')) {
        wp_die(__('Action non autorisée.'));
    }

    // 2. Jeton anti-CSRF obligatoire.
    check_admin_referer('mon_plugin_activer');

    // 3. Nettoyage de la saisie.
    $cle = sanitize_text_field(wp_unslash($_POST['licence'] ?? ''));

    // 4. La clé sert uniquement à l'activation, et n'est pas conservée :
    //    le SDK ne retient que le jeton renvoyé par l'API.
    $resultat = $licence->activate($cle);
    unset($cle);

    // 5. Ne jamais réafficher la clé soumise dans le HTML de retour.
    wp_safe_redirect(add_query_arg('licenceo', $resultat['success'] ? 'ok' : 'ko', wp_get_referer()));
    exit;
});

Pour l'affichage ultérieur, utilisez $licence->license_key_hint() — les quatre derniers caractères — plutôt que de redemander ou de stocker la clé entière.

État public et affichage

L'intégration standard utilise uniquement Licenceo\SDK\SDK. Initialisez une seule instance par produit et par site WordPress, au chargement du fichier principal.

// Dans votre écran de réglages, après contrôle de la capacité de l'utilisateur :
$active = $licence->is_activated(); // un jeton est conservé localement
$valid = $licence->is_valid();     // dernier verdict confirmé par Licenceo
$status = $licence->status();     // aucune requête HTTP
$message = $licence->status_message();
$domain = $licence->domain();

// Échapper les valeurs à l'affichage. La date vient du serveur, jamais d'un calcul local.
echo esc_html($message);
echo esc_html($domain);
if ($status['expires_at'] !== null) {
    echo esc_html(wp_date(get_option('date_format'), strtotime($status['expires_at'])));
}

// Après vérification de capacité + nonce, sur une action explicite de l'utilisateur :
// $result = $licence->activate($cle_saisie);
// $result = $licence->refresh_status(); // résultat de la tentative, pas le statut
// $result = $licence->deactivate();    // libère l'emplacement côté Licenceo

status() retourne toujours ces six champs :

ChampTypeSignification
validboolDernier verdict serveur ; false sans activation.
statusstringactive, expired, revoked, suspended (état de la licence), unknown (jeton non reconnu, ou statut que ce SDK ne connaît pas), ou not_activated (aucune activation locale). Une licence peut rester active mais être inutilisable si le compte éditeur est suspendu.
reasonstring ou nullMotif du refus serveur ; null si valide ou avant activation.
checked_atstring ISO 8601 UTC ou nullRéception du dernier verdict confirmé, y compris celui de l'activation. null avant activation et après désactivation.
expires_atstring ISO 8601 ou nullDate fournie par Licenceo. null pour une licence perpétuelle ou lorsqu'aucune licence n'est authentifiée ; vérifier status avant d'afficher « perpétuelle ».
featuresliste de strings ou nullClés du palier. null = aucune restriction de fonctionnalités, uniquement si la licence est valide. Liste vide sans licence authentifiée. Préférer has_feature() pour autoriser une fonctionnalité.
[
    'valid'      => true,
    'status'     => 'active',
    'reason'     => null,
    'checked_at' => '2026-09-16T10:00:00+00:00',
    'expires_at' => '2027-09-16T10:00:00+00:00',
    'features'   => ['export-csv'],
]

L’activation transmet le product_id configuré. L’API refuse la clé d’un autre produit (404 not_found) avant de créer une activation ; aucune compensation locale n’est nécessaire.

Une activation expirée peut toujours avoir un jeton local : is_activated() n'est pas une autorisation d'accès. is_valid() et has_feature() utilisent le dernier verdict ; ils ne calculent pas l'expiration sur l'horloge du client. Après un renouvellement, la revalidation met à jour la date et la validité. Une panne réseau, un HTTP 429/5xx ou un JSON incomplet conserve tout le dernier statut, y compris checked_at. Le cron horaire réessaiera ; refresh_status() permet une demande explicite. N'appelez pas cette méthode à chaque affichage de page.

status_message() traduit le dernier verdict : expired, suspended, revoked, invalid_token, account_suspended, not_found, invalid_license, limit_reached, already_activated, subscription_required, production_domain_blocked, not_activated. Un code inconnu retourne « Licence invalide. ». Un échec de activate() ne remplace pas l'état d'une licence déjà installée : affichez alors le message du résultat de l'opération.

Licence suspendue

suspended est un statut à part entière, au même titre que active, expired et revoked : l'abonnement de la boutique de l'éditeur est en pause. L'accès est refusé (is_valid() et has_feature() retournent false), mais la licence n'a ni expiré ni été révoquée, et l'activation locale est conservée : elle redevient valide à la reprise, à la prochaine revalidation, sans réactivation. Distinguez-la d'une expiration pour afficher le bon message :

$status = $licence->status();

if ($status['status'] === 'suspended') {
    // Abonnement en pause : inutile de proposer un renouvellement.
} elseif ($status['status'] === 'expired') {
    // Licence arrivée à échéance : proposer de la renouveler.
}

Un verdict suspended remplace immédiatement un ancien verdict active. Un statut que ce SDK ne connaît pas, reçu avec valid: false, est appliqué comme un refus (status vaut alors unknown, le motif du serveur est conservé) : il ne fait jamais conserver l'accès. Seul un verdict valid: true avec le statut active accorde l'accès ; une réponse mal formée (JSON incomplet, page de proxy) conserve, elle, le dernier verdict confirmé, comme une panne réseau. Les versions du SDK antérieures à 1.3.0 ne connaissent pas suspended et traitent ce verdict comme une réponse invalide : mettez le SDK à jour dans vos plugins pour que la pause d'un abonnement coupe bien l'accès.

Revalidation manuelle

refresh_status() suit la même convention que activate(), deactivate() et rotate_token() : un tableau ['success' => bool, 'message' => string]. success décrit la réussite de la revalidation, pas la validité de la licence. Le statut n'est pas dupliqué dans ce résultat : status() le lit localement.

// Dans une action de réglages protégée par capacité utilisateur et nonce :
$result = $licence->refresh_status();

if ($result['success']) {
    $status = $licence->status(); // aucun appel réseau supplémentaire
    if ($status['valid']) {
        // La licence est valide : expiration et fonctionnalités sont à jour.
    } else {
        // Revalidation réussie, mais licence expirée, révoquée ou autre refus métier.
        echo esc_html($licence->status_message());
    }
} else {
    echo esc_html($result['message']);
    $status = $licence->status(); // dernier verdict confirmé, inchangé
}

Un HTTP 200 portant un verdict métier exploitable donne success=true, y compris si valid=false. Le SDK enregistre alors le statut, l'expiration, les fonctionnalités et la date de réception checked_at. Une erreur réseau, un timeout, un HTTP 429/5xx (ou tout HTTP autre que 200) ou une réponse invalide/incomplète donne success=false et ne modifie aucun champ du dernier statut. Le message de l'opération explique la réussite ou l'impossibilité de revalider ; status_message() décrit la licence.

Sans activation locale, success=false, un message le précise et aucune requête n'est envoyée. Sinon, chaque tentative effectue exactement un appel HTTP, sans relance automatique. Le cron utilise la même logique et continue sa revalidation horaire. Ne comparez pas les checked_at pour détecter la réussite : deux verdicts reçus dans la même seconde peuvent avoir la même date.

Version Insights (adoption de version)

activate() et refresh_status() transmettent automatiquement à Licenceo la version réellement exécutée (version fourni à SDK::init(), jamais la dernière version disponible ou téléchargée), get_bloginfo('version') et PHP_VERSION. Rien à appeler soi-même : c'est un enrichissement des mêmes requêtes que le SDK effectue déjà (activation initiale, puis revalidation horaire par cron et à chaque chargement de l'admin si le dernier contrôle date de plus d'une heure) — aucune requête réseau supplémentaire n'est ajoutée pour cette fonctionnalité.

Une mise à jour du plugin change simplement la valeur de version passée à SDK::init() dans le nouveau code déployé ; comme WordPress recharge le fichier du plugin à chaque requête, la prochaine revalidation transmet naturellement la nouvelle version. C'est ce qui permet à Licenceo de détecter une transition (par exemple 2.3.2 → 2.4.0) sans aucun code supplémentaire côté intégrateur.

Confidentialité : uniquement des numéros de version (produit, WordPress, PHP), jamais de contenu de site, de données utilisateur ou de chemin serveur. Le tableau de bord Licenceo affiche les statistiques d'adoption qui en résultent (répartition par version, T50/T80/T95…) — voir licenceo.app/docs/api#version-insights.

Lifecycle WordPress

Pour un plugin, SDK::init() enregistre automatiquement le hook de désactivation WordPress à partir de plugin_file. Passez __FILE__ depuis le fichier principal du plugin (le SDK accepte aussi plugin_basename(__FILE__) et normalise les deux). Ce hook appelle on_deactivation($network_wide = false) et supprime tous les événements du cron SDK. Il ne désactive pas la licence, ne fait aucun appel API et conserve les options pour la prochaine activation du plugin. Le chargement suivant du plugin réinstalle le cron.

Pour un thème, le cycle de vie reste explicite dans functions.php :

add_action('switch_theme', function () use ($licence) {
    $licence->on_deactivation();
});

deactivate() concerne exclusivement la licence : succès confirmé (ou activation confirmée absente) → suppression du jeton, de l'aperçu de clé et du statut, puis invalidation du cache des mises à jour. Il ne désactive ni le plugin ni son cron.

Domaine et multisite

domain() renvoie le host de home_url() en minuscules, sans www., port ou chemin, comme la normalisation de l'API. Cette même valeur est envoyée pour l'activation, la validation, la désactivation, la rotation, les mises à jour et les rapports de plantage. Une instance appartient au site où elle a été initialisée. Après switch_to_blog(), initialisez une instance pour le site courant et restaurez le contexte avec restore_current_blog() ; réutiliser l'ancienne instance pour accéder à une licence lève une LogicException plutôt que mélanger domaines et jetons.

Licenceo compte une activation par domaine. En multisite (depuis 1.4.1), le jeton, la fin de clé et le statut sont donc stockés au niveau du réseau, par domaine : dans un réseau en sous-répertoires (client.fr/a, client.fr/b), tous les sites partagent une seule activation, et activer ou désactiver depuis l'un vaut pour tous. Un réseau en sous-domaines ou à domaines mappés garde une activation par domaine. Une activation enregistrée par une version antérieure au niveau d'un site est reprise automatiquement. La désactivation réseau du plugin nettoie le cron de chaque site, sans révoquer les licences.

Noms des options et transients

Le préfixe est licenceo_<libellé>_<empreinte> : libellé normalisé et limité à 24 caractères, empreinte SHA-256 tronquée à 32 caractères du tableau JSON [api_url sans slash final, product_id en string, type, slug, plugin_file normalisé ou stylesheet]. La version installée n'entre pas dans l'identité. Le libellé est le slug par défaut ; option_prefix permet de le remplacer mais ne désactive jamais l'isolation. Deux slugs qui se normalisent pareil restent distincts. Ne changez pas ces paramètres une fois distribué sans prévoir une réactivation.

Suffixes : _activation_token (string), _license_key_hint (string, 4 caractères), _license_status (tableau ci-dessus), _remote_version (transient de mise à jour), _crash_reported_at (transient anti-spam). Le hook cron utilise _license_check. Les trois options sont écrites sans autoload. En multisite, elles sont stockées comme options réseau (get_site_option), avec en plus un suffixe dérivé du domaine. Il n'y a pas de migration des anciens préfixes de développement : libérez les anciennes activations depuis Licenceo puis réactivez avec le nouveau SDK. La clé complète n'est jamais persistée.

Utilisation — Thème

Mêmes clés, sauf plugin_file remplacé par type => 'theme'. Le stylesheet est détecté automatiquement via get_template() (le thème parent — correct même si l'utilisateur final active un thème enfant par-dessus) :

$licence = \Licenceo\SDK\SDK::init([
    'api_url'    => 'https://api.licenceo.app/api',
    'product_id' => 12,
    'slug'       => 'mon-super-theme',
    'type'       => 'theme',
    'version'    => wp_get_theme(get_template())->get('Version'), // version installée de votre thème
    // 'stylesheet' => get_template(), // optionnel, auto-détecté
]);
add_action('switch_theme', function () use ($licence) {
    $licence->on_deactivation();
});

Compatibilité

  • PHP >= 7.4
  • WordPress >= 5.0 (fonctions wp_remote_*, transients, hooks d'update standards)

Plusieurs produits basés sur ce SDK sur le même site

Chaque produit embarque sa propre copie du SDK. Une classe PHP ne se déclarant qu'une fois, une seule copie fournit le code à tous les produits du site ; chacun garde son propre SDK::init($config), ses options et son jeton.

  • La copie la plus récente de la même version majeure est utilisée (depuis 1.4.0). Chaque copie s'inscrit dans un registre ; une fois toutes les extensions et le thème chargés (wp_loaded), la plus récente est mémorisée dans l'option licenceo_sdk_newest_v{majeure}, et c'est elle qui fournit les classes dès la page suivante, quel que soit l'ordre de chargement.
  • Copies installées par Composer (depuis 1.4.3) : Composer n'exécute licenceo-sdk.php qu'une fois pour tous les plugins qui l'embarquent, si bien que les autres copies ne s'inscrivaient jamais. Le registre interroge désormais aussi chaque autoloader Composer enregistré (Composer 2) : require __DIR__.'/vendor/autoload.php' suffit, rien d'autre à ajouter. Prérequis : la copie exécutée la première doit être en 1.4.3 ou plus ; une copie 1.4.2 ou antérieure chargée en premier ne voit pas les autres.
  • Premier chargement : juste après l'installation d'une extension embarquant un SDK plus récent, la copie incluse en premier sert encore pour cette page, et une notice d'administration l'indique. Elle disparaît dès la page suivante.
  • Chemin vérifié : le chemin mémorisé n'est suivi que s'il reste dans le dossier des extensions ou des thèmes et si la copie annonce bien la version attendue. Sinon, la copie courante est utilisée.
  • Jamais d'autre version majeure : une 2.x ne remplace jamais une 1.x, ses méthodes publiques pouvant différer.
  • Aucune erreur fatale : la redéclaration de classe est détectée et évitée, quel que soit le nombre de copies présentes.
  • Copies 1.3.0 et antérieures : elles ne connaissent pas ce mécanisme. Si l'une d'elles est chargée en premier, elle fournit encore les classes. Mettez à jour le SDK dans tous vos produits.

La version de la copie chargée est lisible dans la constante LICENCEO_SDK_VERSION et dans \Licenceo\SDK\SDK::VERSION.

Ces comportements sont vérifiés en processus PHP isolé (tests/IsolatedLoadTest.php), pas seulement par lecture du code.

Politique de compatibilité

Puisque la copie la plus récente sert tous les produits, la compatibilité ascendante est une règle, pas une intention :

  • Dans une version majeure, aucune méthode publique n'est retirée, renommée ni ne change de signature ou de valeur de retour. Les clés de configuration existantes gardent leur sens.
  • Une nouvelle méthode arrive dans une version mineure. Si votre produit s'en sert et peut cohabiter avec une copie plus ancienne chargée par un autre éditeur, testez-la : method_exists($licence, 'nouvelle_methode') ou version_compare(\Licenceo\SDK\SDK::VERSION, '1.4.0', '>=').
  • Une rupture n'arrive que dans une version majeure, qui aura son propre espace de noms pour cohabiter avec la précédente.

Vérification hors ligne

Le SDK revalide la licence toutes les heures en tâche de fond. Une panne réseau ou une erreur serveur ne change jamais le statut : le dernier verdict est conservé. Au-delà de max_offline_days jours (30 par défaut) sans aucun verdict reçu, is_valid() et has_feature() renvoient false et une notice explique qu'il faut rétablir la connexion à Licenceo ; le jeton est conservé et la première vérification réussie rétablit la licence, sans réactivation. 0 désactive cette limite.

Note d'implémentation. La classe SDK vit dans src/SDK.php et non dans licenceo-sdk.php, et ce n'est pas un détail de rangement : PHP lie une déclaration de classe sans parent dès la compilation du fichier qui la contient. Une classe déclarée dans le fichier portant le garde-fou class_exists() existerait donc déjà quand ce garde-fou s'exécute — il se déclencherait au tout premier chargement, aucun fichier de src/ ne serait chargé, et SDK::init() échouerait sur « Class Licenceo\SDK\Client not found ». C'était le cas jusqu'à la version 1.1.0. Ne remettez pas de déclaration de classe dans le point d'entrée.

Tests

La suite est autonome : PHPUnit est une dépendance de développement de ce dépôt. Elle nécessite PHP 8.3 ou supérieur et Composer ; le SDK distribué conserve PHP 7.4 comme minimum d’exécution. Les fonctions WordPress utilisées (wp_remote_*, options, transients, hooks) sont simulées, sans dépendance à l’application Licenceo.

composer install
vendor/bin/phpunit

Ce qui est couvert : construction des requêtes (méthode HTTP, en-tête X-License-Key, corps JSON, absence de clé dans les URL), activation/désactivation, validation, recherche de version, cache, conservation du dernier statut connu en cas de panne réseau ou d'erreur serveur, paliers de fonctionnalités.

Verrou de mise à jour : les tests unitaires couvrent les caches chaud/froid, le verrou détenu, sa libération sur succès/erreur/exception, la reprise après expiration, la relecture après acquisition, le multisite et les deux modes de cache objet. Le scénario réentrant simule un entrelacement ; ce n'est pas un test de concurrence multiprocessus.

UpdaterLockDatabaseTest exécute les requêtes SQL de production sur MySQL via plusieurs connexions indépendantes : unicité de la clé, absence d'écrasement d'un bail actif, reprise conditionnelle à expiration, horloge SQL et suppression réservée au propriétaire. Ces tests de sémantique SQL sont optionnels et nécessitent pdo_mysql ainsi qu'un compte de test autorisé à créer/supprimer une base. Ils créent une base isolée licenceo_sdk_test_<aléatoire>, supprimée à la fin ; ils ne doivent pas utiliser un compte de production.

LICENCEO_TEST_MYSQL_DSN='mysql:host=127.0.0.1;port=3306' \
LICENCEO_TEST_MYSQL_USER='sdk_test' \
LICENCEO_TEST_MYSQL_PASSWORD='mot-de-passe-de-test' \
vendor/bin/phpunit

Sans ces variables, les trois tests MySQL sont signalés comme ignorés. Ils vérifient les primitives réelles, sans prétendre lancer des requêtes PHP simultanées ; l'exclusion entre processus repose sur la contrainte unique et l'évaluation atomique du prédicat d'UPDATE par MySQL.

Chargement réel du SDK (tests/IsolatedLoadTest.php) : ces tests lancent un vrai processus php neuf, sans aucune classe préchargée, et chargent le SDK comme le ferait un plugin. Ils couvrent les deux modes d'installation — copie manuelle et autoload Composer réellement généré — ainsi que l'initialisation avec une configuration valide puis incomplète, et la coexistence de deux copies physiques distinctes : même version, versions différentes, ordre de chargement, notice d'administration et absence de notice pour un visiteur.

Ces tests existent parce qu'un test unitaire à classes préchargées n'aurait rien prouvé : c'est exactement par là qu'un défaut réel était passé (le SDK ne se chargeait sur aucune installation, cf. section Compatibilité).

Tests de bout en bout sur un vrai WordPress (tests/e2e/, Docker requis) : WordPress et WP-CLI réels, base MariaDB, API Licenceo simulée. Chaque scénario est une vraie requête WordPress (wp eval-file), avec WP_DEBUG actif ; un avertissement ou une dépréciation PHP venant du SDK fait échouer le test.

tests/e2e/run.sh            # PHP 8.3
PHP=7.4 tests/e2e/run.sh    # PHP 7.4, version minimale supportée

Mode multisite : MULTISITE=1 tests/e2e/run.sh monte un réseau en sous-répertoires, active la licence sur le site principal et vérifie que le sous-site la partage.

Remontée des plantages : CRASH=1 tests/e2e/run.sh provoque de vraies erreurs fatales et vérifie les rapports reçus par l'API simulée (plugin foo et son voisin foobar sans SDK, plugin mono-fichier, thème, plugin relié par lien symbolique, erreur hors de tout produit, message sans chemin serveur ni e-mail). WP_HANDLER=off rejoue le même scénario avec le gestionnaire d'erreurs fatales de WordPress désactivé.

Autres modes :

COMPOSER_PLUGINS=1 tests/e2e/run.sh                 # deux vrais plugins avec le SDK installé par Composer
NEWER_IN=first COMPOSER_PLUGINS=1 tests/e2e/run.sh  # idem, copie la plus récente chargée en premier
MULTISITE=1 SUBDOMAINS=1 tests/e2e/run.sh           # réseau en sous-domaines : une activation par domaine
OBJECT_CACHE=1 tests/e2e/run.sh                     # scénario complet avec Redis comme cache objet

Scénarios couverts : deux plugins embarquant des copies différentes du SDK (la plus récente est chargée dès la deuxième requête, sans erreur fatale), activation et refus de clé, revalidation planifiée par WP-Cron pour chaque produit, injection d'une mise à jour dans le transient de WordPress, panne de l'API sans perte de licence, limite de 30 jours hors ligne, retour de l'API, mise à jour réelle (paquet signé téléchargé et installé par l'upgrader groupé de WordPress, licence conservée, mise à jour plus proposée ensuite), rotation de jeton dont la réponse se perd puis relancée par WP-Cron, révocation, désactivation du plugin (cron supprimé, jeton conservé).

Limites connues :

  • aucun test contre l'API Licenceo réelle : l'API est simulée (tests/e2e/mock/router.php) avec les mêmes en-têtes et formats de réponse ;
  • la coexistence est vérifiée sur des copies dont seule la version diffère ; la politique de compatibilité (section Compatibilité) garantit que les méthodes publiques d'une version majeure ne changent pas.

Documentation complète

La référence de l'API REST sous-jacente (endpoints, authentification, webhooks) est disponible sur licenceo.app/docs/api.

Licence

Ce SDK est distribué sous licence GPL-2.0-or-later, cohérente avec l'écosystème WordPress. Voir LICENSE et la notice juridique du SDK pour le détail de ce que cette licence couvre (le code du SDK) et ne couvre pas (le SaaS Licenceo lui-même, son API, sa marque).

Support

Questions, bugs : contact@licenceo.app.

Releases

Les deux versions internes, $licenceo_sdk_copy_version dans licenceo-sdk.php et SDK::VERSION dans src/SDK.php, doivent toujours rester identiques.

Avant de publier une version depuis ce dépôt :

composer validate --strict
composer install
vendor/bin/phpunit

La publication est réalisée via les tags du dépôt public GitLab et Packagist. Le fichier composer.json ne contient pas de champ version : Packagist utilise les tags Git. Ne modifiez pas un tag déjà publié pour y remplacer le code.