Live · status OK
Documentation · OW Consent v1.4.3

OW Consent
Documentation

Le consentement WordPress qui bloque les traceurs avant le clic, pas après.

v1.4.3GPL-2.0-or-laterDocumentation

OW Consent — Documentation

La gestion du consentement complète dans WordPress : bannière multi-juridictions, blocage des traceurs, scanner, documents légaux, registre de preuve et portail de droits. Auteur : OptionWeb — Julien Daniel Page du plugin : https://optionweb.dev/fr/addons/ow-consent/ Licence : GPL-2.0-or-later Version couverte par ce document : 1.4.3


Table des matières

  1. Vue d'ensemble
  2. Installation
  3. Démarrage rapide
  4. Les onze profils de conformité
  5. Les catégories de cookies
  6. La bannière
  7. Le blocage automatique
  8. Le scanner de traceurs
  9. Le générateur de documents légaux
  10. Le registre de consentement
  11. Le portail de droits (DSAR)
  12. L'opt-out CCPA « Do Not Sell or Share »
  13. Google Consent Mode v2
  14. IAB TCF v2.2
  15. Global Privacy Control
  16. La détection régionale
  17. L'intégration OW Forms
  18. Le bouton flottant
  19. Shortcodes
  20. API REST
  21. Référence des réglages
  22. Dépannage
  23. FAQ

Vue d'ensemble

OW Consent est une suite de gestion du consentement pour WordPress. Elle couvre la chaîne entière : afficher un choix, l'appliquer réellement aux traceurs, en garder la preuve, publier les documents qui l'expliquent, et recevoir les demandes des personnes concernées.

Onze profils de conformité sont livrés — du RGPD à la CCPA en passant par la Loi 25 québécoise, la LGPD brésilienne ou la DPDP indienne. Le profil actif décide du modèle de consentement (opt-in ou opt-out), des valeurs par défaut de Google Consent Mode, du texte des documents générés, des droits publiés et de l'autorité de contrôle citée.

Tout tient dans votre site. Le plugin ne contacte qu'un seul service externe, la Global Vendor List d'IAB Europe, et seulement si vous activez le module TCF, qui est désactivé par défaut. Aucune télémétrie, aucun compte, aucun abonnement.

Compatible avec un cache de page complet, par construction

C'est le point d'architecture qui gouverne tout le reste. Rien de ce que le serveur rend ne dépend du cookie de consentement. Le HTML est identique pour tous les visiteurs : les traceurs sont réécrits en balises inertes pour tout le monde, et c'est un runtime JavaScript qui les libère dans le navigateur, catégorie par catégorie, en lisant le cookie avant le premier rendu.

Conséquence pratique : LiteSpeed Cache, WP Rocket, Varnish ou un CDN ne peuvent pas servir les choix d'un visiteur à un autre. Seules les réponses réellement personnelles — la page de confirmation d'identité d'une demande de droits, les réponses REST qui contiennent des données du visiteur — sont explicitement marquées non cachables. Aucun nonce n'est imprimé dans du HTML cachable non plus : la bannière en récupère un frais sur un endpoint no-store juste avant chaque écriture.

Ce qui est livré

  • Bannière de consentement : quatre positions, thème clair / sombre / automatique, « Tout refuser » aussi visible que « Tout accepter », panneau de préférences accessible au clavier, entièrement traduisible.
  • Blocage automatique des traceurs : scripts, snippets en ligne, iframes, pixels, indices de ressources (preconnect, dns-prefetch, preload), feuilles de style analytics/marketing et médias tiers, à partir de 175 signatures livrées et éditables.
  • Scanner de traceurs : balayage de vos propres pages par WP-Cron, lecture des en-têtes Set-Cookie, sonde navigateur réservée aux administrateurs, et un panneau qui dit toujours ce que le balayage a réellement couvert.
  • Générateur de documents légaux : politique de confidentialité et politique cookies pour les onze profils, dans la langue de la juridiction ; mentions légales et CGU/CGV en français.
  • Registre de consentement : chaque action ajoutée à une table chaînée par HMAC, avec le profil en vigueur, l'empreinte des documents publiés et l'empreinte de la bannière réellement affichée.
  • Portail de droits (DSAR) : formulaire par shortcode, vérification par e-mail, délai de l'article 12.3 déclenché à la confirmation d'identité, branchement sur l'exportateur et l'effaceur natifs de WordPress.
  • Google Consent Mode v2, IAB TCF v2.2, Global Privacy Control, détection régionale, intégration OW Forms : chacun activable indépendamment.
  • API REST complète sous le namespace owc/v1.

Ce qu'il ne fait pas — à lire avant de s'engager

Sur un sujet juridique, une promesse excessive vous expose autant qu'elle expose l'éditeur. Voici les limites, telles qu'elles sont écrites dans le code.

  • Les documents générés sont des modèles, pas un conseil juridique. Chaque document se termine par un avertissement qui le dit, et cet avertissement est activé par défaut. Faites relire vos documents avant de les publier.
  • Le module TCF n'est pas un CMP enregistré auprès d'IAB Europe. Il exige un CMP ID que vous devez obtenir vous-même, il n'offre aucun choix au niveau vendeur, et les vendeurs sont fondés à rejeter son signal. Si un revenu publicitaire sous TCF compte pour vous, utilisez un CMP certifié.
  • Le registre n'est infalsifiable que sous condition. La chaîne n'est une preuve que si sa clé de signature vit hors de la base de données. Le plugin détecte le cas contraire, le rapporte lui-même et vous le dit dans l'administration au lieu de prétendre l'inverse.
  • Le scanner n'exécute aucun JavaScript. Il voit ce que votre HTML contient et ce que vos en-têtes Set-Cookie déposent ; ce qu'un gestionnaire de balises injecte au runtime n'est vu que par une sonde réservée aux administrateurs connectés.
  • Le blocage par défaut n'est pas « tout ce qui est tiers ». Un script tiers inconnu est autorisé par défaut ; ce sont les iframes inconnues qui sont bloquées par défaut. Le blocage repose sur le catalogue de signatures, que vous pouvez enrichir.
  • Sans JavaScript, aucun choix ne peut être enregistré. Le visiteur voit un bloc <noscript> et rien de non essentiel n'est chargé, mais rien n'est enregistré non plus.
  • Pas de granularité état par état aux États-Unis ni province par province au Canada dans la détection régionale : tout le territoire américain reçoit le profil ccpa, tout le Canada reçoit quebec.
  • Mentions légales et CGU/CGV n'existent qu'en français. Pour toute juridiction dont la langue cible n'est pas le français, le générateur refuse de produire ces deux documents au lieu de publier un texte inadapté.
  • Aucun rôle dédié : toute l'administration exige la capacité manage_options.

Cette liste est un choix assumé, pas une roadmap masquée. Sur la conformité, un outil qui énonce ses trous vaut mieux qu'un outil qui les cache.


Installation

Depuis le .zip

  1. Téléchargez ow-consent-1.4.3.zip depuis https://optionweb.dev/fr/addons/ow-consent/
  2. Extensions → Ajouter → Téléverser une extension
  3. Choisissez le fichier, cliquez sur Installer, puis sur Activer

Par FTP

Décompressez l'archive et déposez le dossier ow-consent dans /wp-content/plugins/, puis activez l'extension depuis Extensions.

Prérequis

  • WordPress 6.2 ou plus récent — c'est un refus de démarrage, pas une recommandation (voir ci-dessous)
  • PHP 7.4 ou plus récent
  • MySQL 5.7+ / MariaDB 10.2+
  • WP-Cron fonctionnel si vous utilisez le scanner, la rétention du registre, les rappels d'échéance DSAR ou le module TCF

Le garde-fou WordPress 6.2. Depuis la 1.3.0, chaque identifiant de table est lié par le marqueur %i de wpdb::prepare(), que WordPress ne comprend qu'à partir de 6.2. Sur un cœur plus ancien, prepare() renverrait une chaîne vide, le catalogue de traceurs se résoudrait silencieusement à un ensemble vide, et la bannière continuerait de promettre au visiteur un blocage qui n'aurait pas lieu. Le plugin refuse donc de démarrer et affiche un message d'erreur qui dit explicitement que rien n'est bloqué et qu'aucun consentement n'est enregistré. L'en-tête Requires at least empêche déjà l'activation en dessous de 6.2, mais il ne couvre ni une rétrogradation du cœur sous une installation en service, ni une copie déposée par FTP.

Ce qui est installé

À l'activation, OW Consent crée quatre tables :

TableContenu
{prefix}owc_ledgerLe registre de consentement, chaîné par hachage
{prefix}owc_dsarLes demandes d'exercice de droits
{prefix}owc_scannerLes trouvailles du scanner de traceurs
{prefix}owc_scriptsLe catalogue de signatures utilisé par le blocage

Une cinquième table, {prefix}owc_form_links, est créée séparément par l'intégration OW Forms si le plugin frère est actif (voir L'intégration OW Forms).

L'activation ajoute aussi :

  • l'option owc_settings, seedée vide — délibérément : tant que vous n'avez rien enregistré, tous les textes viennent des défauts anglais traduits à la volée dans la langue du site, au lieu de figer dans la base la locale de la personne qui a activé le plugin ;
  • l'option owc_version ;
  • le catalogue de traceurs livré (data/tracker-catalog.json), inséré par paquets de 100 en INSERT IGNORE, avec un drapeau de seed par version (owc_catalog_seeded_1.4.3) ;
  • la tâche quotidienne owc_daily_maintenance, planifiée une heure après l'activation.

Cinq autres événements cron sont armés par les modules qui les utilisent. Les six hooks cron du plugin sont : owc_scanner_run, owc_scanner_run_batch, owc_ledger_retention, owc_tcf_refresh_gvl, owc_run_upgrade et owc_daily_maintenance.

En multisite, les tables sont créées par site, jamais partagées (wp_2_owc_ledger, etc.). Une activation réseau parcourt tous les sites uniquement si le réseau compte au plus 200 sites ; au-delà, chaque site est provisionné paresseusement à sa première requête. Un sous-site créé après une activation réseau est provisionné par le hook wp_initialize_site.

À définir dans wp-config.php

Aucune de ces constantes n'est obligatoire, mais deux d'entre elles changent la valeur probante de ce que le plugin produit.

// Recommandé : sort la clé de signature du registre de la base de données.
define( 'OWC_LEDGER_KEY', 'une longue chaîne aléatoire, propre à ce site' );

// Recommandé : les salts WordPress standards. Sans eux, WordPress les stocke en base,
// et le registre le signale lui-même comme non infalsifiable.
define( 'AUTH_KEY',  '…' );
define( 'AUTH_SALT', '…' );

// Si le site est derrière un CDN, un répartiteur de charge ou un reverse proxy.
// Sans cette constante, CF-Connecting-IP, X-Forwarded-For et X-Real-IP sont IGNORÉS
// et seul REMOTE_ADDR est utilisé — ce qui donne la même identité à tous vos visiteurs
// pour la limitation de débit.
define( 'OWC_TRUSTED_PROXY', '198.51.100.0/24, 2001:db8::/32' );
// `OWC_TRUSTED_PROXIES` est accepté comme alias ; une chaîne ou un tableau conviennent.

// Détection régionale seulement : déclare quels en-têtes de pays sont dignes de confiance.
define( 'OWC_GEO_TRUSTED_HEADERS', 'cloudflare' ); // 'cloudflare'|'cloudfront'|'proxy'|'all'
define( 'OWC_BEHIND_CLOUDFLARE', true );
define( 'OWC_BEHIND_CLOUDFRONT', true );

OWC_LEDGER_KEY sert aussi à dériver la clé du cookie de détection régionale. Si elle n'est pas définie, la clé du registre retombe sur wp_salt('auth').

Qui a accès

Toute l'administration du plugin et toutes les routes REST d'administration exigent la capacité manage_options. Il n'existe ni rôle dédié, ni capacité plus fine : ouvrir l'écran du plugin à quelqu'un revient à lui donner les réglages du site.

Chemin de mise à jour

Le plugin ne migre jamais son schéma en ligne sur une page anonyme. Quand la version change, owc_version et un drapeau owc_pending_upgrade sont écrits immédiatement, puis :

  • la migration s'exécute inline si la requête est une requête d'administration hors AJAX, une exécution de cron ou une commande WP-CLI ;
  • sinon un événement owc_run_upgrade est planifié cinq secondes plus tard.

admin-ajax.php est traité comme une requête anonyme : c'est un point d'entrée public. La migration s'exécute sous verrou (owc_upgrade_lock, volé au bout de 300 secondes), et un filet sur admin_init rattrape les sites où WP-Cron est désactivé. Sur une version identique, le coût total est un seul get_option().

Désactivation et suppression

Désactiver conserve toutes les données et n'efface que les six événements cron. En désactivation réseau, tous les sites sont parcourus par lots de 200 — contrairement à l'activation — parce qu'un cron laissé armé ne disparaîtrait jamais.

Supprimer le plugin déclenche uninstall.php, qui procède en deux temps :

  1. Toujours, quel que soit votre réglage : les six crons sont effacés et la table {prefix}owc_dsar est supprimée, avec ses transients de limitation de débit. C'est la seule table qui contient des données personnelles directement identifiantes concernant des tiers (adresse e-mail, nom, texte libre) ; une fois le plugin parti, plus rien ne borne sa rétention et plus aucun écran ne permet d'y répondre, de l'exporter ou de l'effacer. Exportez les demandes que vous devez conserver avant de supprimer le plugin.
  2. Seulement si delete_data_on_uninstall est explicitement activé : suppression des tables owc_ledger, owc_dsar, owc_scanner, owc_scripts et owc_form_links, des options nommées, de toutes les options préfixées owc_ (transients compris) et des usermeta préfixées owc_. En multisite, les options réseau suivent la décision du site principal.

Ce réglage est désactivé par défaut : la preuve de consentement, exigée par l'article 7.1 du RGPD, survit à la suppression du plugin.


Démarrage rapide

Après activation, ouvrez l'entrée OW Consent dans la barre latérale de l'administration. Tout le plugin tient sur cet écran unique, en dix onglets : Dashboard, Banner, Compliance, Legal identity, Categories, Policies, Scanner, Tracker catalogue, Audit ledger, DSAR requests.

Une sauvegarde ne concerne que l'onglet ouvert. C'est délibéré : chaque booléen a un champ caché jumeau, et une clé absente du formulaire signifie « ce champ est sur un autre onglet », jamais « décoché ». Sans cela, enregistrer un onglet écraserait les réglages de tous les autres.

1. Choisissez votre profil de conformité

Onglet Compliance. Le profil décide du modèle de consentement, des valeurs par défaut de Consent Mode, des documents générés et des droits publiés. Par défaut : gdpr.

compliance_strict est activé par défaut : c'est ce qui étend le blocage au-delà des seuls scripts enregistrés par WordPress. Laissez-le actif si vous voulez que les iframes, les pixels et les scripts écrits en dur dans votre thème soient traités.

2. Remplissez l'identité légale

Onglet Legal identity. Ces champs sont la matière première des documents générés, et la génération est refusée tant qu'un champ obligatoire est vide — avec la liste des clés manquantes, pas un échec silencieux.

Minimum pour tous les documents : legal_company_name et legal_company_email. Pour tout sauf la politique cookies, ajoutez l'adresse et le pays. Pour les mentions légales, ajoutez aussi le téléphone, le directeur de la publication et les coordonnées complètes de l'hébergeur ; en France, en Belgique et au Luxembourg, la forme juridique et le numéro d'immatriculation deviennent obligatoires eux aussi.

Laissez legal_dpa_authority vide : l'autorité de contrôle est dérivée de votre pays et de votre profil. La renseigner à la main sur un site multi-juridictions revient à nommer le mauvais régulateur.

3. Vérifiez vos catégories

Onglet Categories. Les six catégories sont disponibles par défaut. Désactivez celles que votre site n'utilise pas : une catégorie absente de l'interface n'est pas un gain de conformité, c'est une ressource bloquée pour toujours sans interrupteur pour la libérer — le blocage la reverse alors sur marketing.

Laissez les libellés vides tant qu'ils vous conviennent : ils suivent alors la langue du site. Dès que vous personnalisez un texte, ce texte cesse de suivre la langue.

4. Réglez la bannière

Onglet Banner. Position, thème, boutons, libellés, durée de renouvellement.

Deux points à ne pas manquer : laissez banner_reject_all activé (« Refuser » doit être aussi simple et visible qu'« Accepter »), et laissez la croix de fermeture désactivée — elle est absente par défaut parce qu'une fermeture sans choix équivaut à un refus implicite. Si vous l'activez, un clic sur la croix exécute le chemin « Tout refuser » complet, jamais une fermeture silencieuse.

5. Générez vos documents

Onglet Policies. Quatre documents : politique cookies, politique de confidentialité, mentions légales, CGU/CGV. Chacun devient une page WordPress versionnée, dont le lien est réinjecté dans les réglages.

Générez d'abord en prévisualisation, relisez, puis publiez. Et faites relire par un professionnel : le bandeau d'avertissement en bas de chaque document n'est pas décoratif.

6. Ouvrez le portail de droits

Créez une page et collez-y [owc_dsar_form]. Renseignez dsar_email dans l'onglet DSAR requests : c'est l'adresse de contact publiée sous le formulaire et le destinataire des notifications. Sans elle, le plugin retombe sur legal_dpo_email, puis sur admin_email — mais admin_email n'est jamais publié sur une page publique.

7. Vérifiez avant d'ouvrir au public

Le tableau de bord exécute quinze contrôles de conformité et distingue les erreurs des avertissements : bannière désactivée, « Tout refuser » absent, croix de fermeture activée, identité légale incomplète, portail de droits fermé, registre désactivé, aucune politique liée, autorité de contrôle en contradiction avec le profil actif (avec un bouton « Fix this » qui la réinitialise), site pas en HTTPS, traceurs non catégorisés, demandes DSAR hors délai, règles de catalogue qui ne peuvent pas se déclencher.

Passez ces quinze lignes au vert avant d'annoncer que votre site est conforme.


Les onze profils de conformité

Le profil de conformité n'est pas cosmétique. Il pilote le modèle légal, l'interface, les signaux techniques et le contenu des documents publiés.

La liste

ProfilRégime viséModèle
gdprRGPD + ePrivacy (UE/EEE)Opt-in
uk_pecrUK GDPR + PECR (Royaume-Uni)Opt-in
ch_nfadpnLPD suisseOpt-in
quebecLoi 25 (Québec)Opt-in
lgpdLGPD (Brésil)Opt-in
popiaPOPIA (Afrique du Sud)Opt-in
piplPIPL (Chine)Opt-in
dpdpDPDP Act 2023 (Inde)Opt-in
ccpaCCPA / CPRA (Californie)Opt-out
us_genericLois d'État américaines génériquesOpt-out
auPrivacy Act (Australie)Opt-out

Réglage : compliance_profile, défaut gdpr.

Opt-in signifie qu'avant tout choix, seules les catégories forcées sont accordées. Opt-out signifie que tout est accordé jusqu'au refus. Les trois profils opt-out sont ccpa, us_generic et au. Le runtime JavaScript applique exactement la même règle que PHP, de sorte que serveur et navigateur ne peuvent pas rapporter deux états différents.

Ce que le profil change réellement

Ce qui changeDétail
Modèle de consentementOpt-in, sauf ccpa, us_generic, au
Valeurs par défaut de Consent ModeLes sept signaux passent à granted sous un profil opt-out
Lien « Do Not Sell or Share » obligatoireccpa et us_generic uniquement
GPC juridiquement contraignantccpa et us_generic uniquement — l'Australie est explicitement exclue : régime opt-out, mais qui ne reconnaît pas GPC
Périmètre « le RGPD s'applique » pour TCF27 pays de l'UE + IS, LI, NO + GB + CH, soit 31 codes
Langue du document généréquebec → français ; lgpd → portugais ; gdpr avec un pays FR, BE ou LU → français ; tout le reste → anglais
Autorité de contrôle citéeTable par profil, affinée par pays pour le RGPD
Liste des droits publiéeUne liste rédigée par profil, avec citation d'article
Corps « régime cookies » du document anglaisTexte, périmètre de transfert et garanties distincts par profil

Les documents générés par profil

La politique de confidentialité et la politique cookies existent pour les onze profils. Les gabarits sont résolus dans cet ordre, premier trouvé gagnant : <type>_<profil>_<langue>, puis <type>_<profil>, puis <type>_<langue>, puis <type>.

Les corps anglais de la politique cookies sont conscients du profil : règle applicable, périmètre de transfert, garanties et libellé du panneau diffèrent pour gdpr, uk_pecr, ch_nfadp, au, pipl, dpdp, ccpa et us_generic, avec les citations correspondantes (art. 5(3) de la directive 2002/58, PECR reg. 6, art. 45c(b) LTC et art. 19/6(7)(b) nLPD, APP 8, art. 24 PIPL, sections 5/6/7/9(3)/16 du DPDP Act 2023, §1798.121 et Cal. Code Regs. tit. 11 §7025, VCDPA/CPA/CTDPA/UCPA/TDPSA).

Autorité de contrôle citée

ProfilAutorité nommée
gdpr, pays FRCNIL
gdpr, pays BEAPD-GBA
gdpr, pays LUCNPD
gdpr, pays DEBfDI
gdpr, autre pays EEEFormulation générique (« l'autorité de contrôle compétente »)
uk_pecrICO
ch_nfadpPFPDT / FDPIC
quebecCommission d'accès à l'information
lgpdANPD
ccpaCalifornia Privacy Protection Agency
popiaInformation Regulator (South Africa)
piplCyberspace Administration of China
dpdpData Protection Board of India
auOAIC
us_genericAttorney General de votre État

Si vous avez modifié legal_dpa_authority, votre saisie l'emporte sur cette dérivation.

Limites de la couverture juridictionnelle

  • Seuls quatre pays de l'EEE ont une autorité nommée (FR, BE, LU, DE). Un site RGPD en Espagne, en Italie ou aux Pays-Bas publie une formulation générique.
  • Pas de granularité état par état aux États-Unis. Le profil us_generic existe et porte ses propres textes VCDPA/CPA/CTDPA/UCPA/TDPSA, mais aucune détection automatique ne l'attribue : il se choisit à la main.
  • Pas de granularité provinciale au Canada.
  • Trois langues de documents seulement : français, anglais, portugais. Les corps ne passent pas par le mécanisme de traduction de WordPress — c'est délibéré : un document légal doit être monolingue et sa langue suit la juridiction, jamais la locale de l'administrateur.
  • Mentions légales et CGU/CGV : gabarits français uniquement. Le générateur refuse net pour toute autre langue cible, à moins que vous ne fournissiez votre propre corps via le filtre owc_policy_template.

Les catégories de cookies

La liste canonique

Six catégories, dans cet ordre d'affichage, necessary toujours en tête :

SlugLibellé livréDescription livrée
necessaryNecessaryStrictly required for the site to function (cart, login, language preferences). Cannot be disabled.
functionalFunctionalEnhance the experience (chat, embedded videos, maps). Without them some features may not work.
analyticsStatisticsHelp us understand how you use the site (anonymously). No personal data is shared for commercial purposes.
marketingMarketingEnable us to show you ads and content tailored to your interests on other sites.
preferencesPreferencesRemember your interface choices (layout, saved filters).
socialSocial & embedsAllow embedded social content (YouTube, Instagram, X) to load.

Les valeurs ci-dessus sont les défauts anglais stockables. Sur un site francophone, le visiteur voit la traduction — voir le mécanisme de traduction plus bas.

Disponibilité

Réglage cat_<slug>_available, un par catégorie. Les six sont disponibles par défaut. L'ancienne clé cat_<slug>_enabled est encore lue en repli, sans migration.

preferences et social sont livrées disponibles parce que le catalogue de seed classe YouTube, Spotify, SoundCloud, Instagram, X et Facebook en social : sans la catégorie dans l'interface, ces intégrations resteraient bloquées définitivement sans moyen d'opt-in.

necessary est forcée : elle ne peut pas être désactivée (art. 5.3 ePrivacy). Elle n'a donc pas d'interrupteur de disponibilité dans l'administration, seulement un libellé et une description.

Le mécanisme de traduction

Réglages cat_<slug>_label et cat_<slug>_desc. La valeur stockée est renvoyée verbatim uniquement si elle est non vide et différente du défaut anglais ; sinon c'est la traduction qui sort.

Conséquence à connaître : tant que vous ne personnalisez rien, changer la langue du site change la langue des catégories. Dès que vous saisissez votre propre texte, ce texte est figé dans la langue où vous l'avez écrit. La même règle s'applique aux textes de la bannière.

La description traduite de necessary est construite à partir de deux chaînes : la seconde nomme les cookies du plugin lui-même, owc_consent (jusqu'à 13 mois) et owc_geo (24 heures, écrit uniquement quand la détection régionale est activée).

Correspondance Google Consent Mode v2 — contraignante

C'est la source de vérité unique du plugin : la bannière, le blocage et le bootstrap lisent tous cette table, ils ne peuvent donc pas diverger.

CatégorieSignaux Consent Mode v2
necessarysecurity_storage, functionality_storage
functionalfunctionality_storage, personalization_storage
analyticsanalytics_storage
marketingad_storage, ad_user_data, ad_personalization
preferencespersonalization_storage
socialad_storage, ad_user_data

Nuance importante : si votre site expose la catégorie functional, functionality_storage est retiré de la liste de necessary. Sinon il serait accordé avant tout consentement, alors qu'il appartient à une catégorie optionnelle.

Correspondance TCF — descriptive uniquement

functionalfunctional, analyticsmeasurement, marketingadvertising, preferencespersonalization, socialsocial_media.

Ce sont des étiquettes d'affichage. La correspondance contraignante côté TCF est la table des identifiants numériques de finalités IAB, décrite dans la section IAB TCF v2.2. Ne branchez jamais un comportement TCF sur ces cinq étiquettes.

Que se passe-t-il si une catégorie disparaît

Si une règle du catalogue pointe vers une catégorie que votre site n'expose plus — vous avez désactivé social, par exemple — le blocage reverse la ressource sur marketing, ou à défaut sur la première catégorie optionnelle disponible. Sans cette normalisation, la ressource serait bloquée pour toujours sans aucun interrupteur pour la libérer.


La bannière

Quand elle s'affiche

La bannière est rendue sur wp_footer en priorité 5, et ses ressources sont mises en file sur wp_enqueue_scripts. Elle sort immédiatement si l'une de ces conditions est vraie : contexte d'administration, flux RSS, robots.txt, ou banner_enabled désactivé. Elle sort également si wp_head n'a jamais été déclenché — un thème qui n'appelle pas wp_head() obtiendrait sinon un balisage inerte et non stylé.

Le HTML est identique pour tous les visiteurs. Toutes les cases du panneau sont rendues à OFF côté serveur puis hydratées côté client depuis le cookie. C'est le JavaScript qui pose <html data-owc="given|none"> de façon synchrone, avant le premier rendu ; le CSS n'affiche la bannière que sur data-owc="none".

Corollaire à connaître : un visiteur sans JavaScript ne voit jamais la bannière, donc il n'est jamais enfermé derrière une modale qu'il ne peut pas fermer. Il voit à la place un bloc <noscript> qui explique que ses préférences ne peuvent pas être enregistrées et qu'aucun cookie non essentiel n'est chargé tant qu'aucun choix n'a été fait.

Positions et thème

RéglageValeursDéfaut
banner_positionbottom-bar, bottom-card, center-modal, top-barbottom-bar
banner_styleauto, light, darkauto

Seul center-modal obtient role="dialog", aria-modal="true", un fond assombri, un piège de focus et la touche Échap. Les trois autres positions sont un role="region" : Échap n'est pas intercepté (le thème garde sa propre gestion) et le focus n'est pas volé au premier rendu, ce qui pousserait un utilisateur clavier au-delà de tous les liens d'évitement.

La palette sombre s'applique sous prefers-color-scheme: dark avec des gardes pour ne pas écraser un thème qui déclare explicitement light. Le thème du site peut piloter la palette par variables CSS : --owc-paper-tint, --owc-ink, --owc-smoke, --owc-fog, --owc-ui, --owc-accent, --owc-accent-strong, --owc-accent-darker.

Détails d'implantation : z-index 99998 pour la bannière et 99997 pour le fond, prise en charge de env(safe-area-inset-*) pour les encoches iOS, décalage automatique sous la barre d'administration WordPress en position haute.

Les boutons

RéglageDéfautEffet
banner_accept_alltrueAffiche « Tout accepter »
banner_reject_alltrueAffiche « Tout refuser »
banner_preferencestrueAffiche « Personnaliser »
banner_close_xfalseAffiche la croix de fermeture
banner_show_logotrueAffiche le logo du site

« Tout accepter » et « Tout refuser » partagent la même classe de mise en forme : même fond, même bordure, même graisse, même remplissage. C'est la réponse à l'exigence CNIL / EDPB (lignes directrices 03/2022) : refuser doit être aussi simple et aussi visible qu'accepter.

La croix de fermeture

Elle est désactivée par défaut, parce qu'une croix de fermeture équivaut à un refus implicite. Trois comportements à connaître :

  1. Quand banner_close_x est off, la croix est présente dans le balisage mais porte l'attribut hidden, et le CSS la retire totalement : ni visible, ni focusable, ni annoncée aux lecteurs d'écran.
  2. Quand elle est activée, cliquer la croix exécute le chemin « Tout refuser » complet, jamais une fermeture silencieuse.
  3. Elle est révélée par le runtime, et ré-étiquetée, dans un seul cas : lorsqu'un refus n'a pas pu être enregistré du tout. Elle ne refuse alors plus rien, elle range l'avis — et cette fermeture n'est pas comptée comme un choix. Un visiteur dont le serveur refuse la décision n'est pas laissé face à une bannière qu'il ne peut pas fermer.

La première couche

  • Le titre (text_title) et le message (text_message). Le message est le seul champ à HTML riche du plugin ; il est rendu avec un wp_kses limité à <a href target rel>.
  • Le lien vers la politique de confidentialité : link_privacy_policy, avec repli sur la page de politique déclarée dans WordPress si le réglage est vide.
  • Le lien vers la politique cookies : link_cookie_policy, sans repli.
  • Le nom du responsable de traitement : legal_company_name, sinon le nom du site, affiché comme « Responsable de traitement : … » (art. 13(1)(a)).
  • Le logo : le logo personnalisé du thème en taille medium, sinon l'icône du site.

Le panneau de préférences

Une ligne par catégorie disponible, dans un role="group" nommé.

  • Une catégorie forcée affiche un badge textuel « Always active », sans interrupteur.
  • Une catégorie optionnelle affiche un <button role="switch"> avec aria-checked, aria-labelledby et aria-describedby. L'état est porté par aria-checked, la position du curseur et un mot visible On/Off — jamais par la couleur seule.
  • Cible tactile agrandie à 44 px sur petit écran (WCAG 2.2 AA, critère 2.5.8).
  • La barre d'actions du panneau est sticky en bas, pour que « Enregistrer mes choix » reste atteignable pendant que la liste défile.

Si le module TCF est actif, deux blocs supplémentaires apparaissent : les finalités TCF atteignables et les fonctionnalités spéciales déclarées. Voir IAB TCF v2.2.

Renouvellement du consentement

RéglageDéfautBornes
consent_renewal_months120 à 13
consent_policy_hash_checktrue

0 ne veut pas dire « ne jamais redemander ». La valeur 0 est convertie en 13 mois, et 13 mois est le plafond dur (délibération CNIL 2020-091). La durée effective est donc toujours comprise entre 1 et 13 mois, et c'est cette même valeur qui gouverne la durée du cookie, le contrôle de péremption côté serveur et la durée publiée dans les documents générés. La constante OWC_COOKIE_TTL visible dans le code source n'est qu'un repli.

consent_policy_hash_check redemande le consentement quand vos documents changent : le cookie porte une empreinte des pages de politique liées, et une différence rouvre la bannière. L'empreinte vaut la chaîne vide quand aucune page n'est liée — c'est ce qui permet de distinguer « inconnu » de « changé » et de ne jamais re-solliciter tout un site sur une comparaison vide.

Les textes

Sept emplacements, tous vides par défaut et donc traduits automatiquement : text_title, text_message, text_accept_all, text_reject_all, text_preferences, text_save, et floating_button_label pour le bouton flottant.

Dans l'administration, chaque champ affiche son défaut traduit en indication de saisie : laisser vide conserve le défaut qui suit la langue du visiteur.

Le filtre owc_banner_texts permet de remplacer ces textes par code. Des emplacements supplémentaires sont acceptés ; les valeurs non scalaires ou vides sont écartées pour qu'un rappel maladroit ne puisse pas vider la bannière.

Comment le choix est réellement enregistré

Le runtime JavaScript est en ES5 strict — pas de fonctions fléchées, pas de littéraux de gabarit — pour fonctionner dans les navigateurs intégrés aux applications et les vieux WebViews. Sa séquence d'écriture mérite d'être connue, parce que c'est elle qui explique la plupart des messages d'erreur.

  1. Ré-ancrage de l'origine. Les URLs REST viennent de la configuration WordPress. Si vos visiteurs naviguent sur un autre hôte (www contre apex, alias, domaine de prévisualisation, proxy qui réécrit Host), cette URL est cross-origin et le navigateur refuse de stocker le Set-Cookie pendant que WordPress répond 200. Le runtime ré-ancre donc le chemin fourni par PHP sur l'origine réellement parcourue.
  2. Nonce. Récupéré frais sur GET /owc/v1/nonce juste avant l'écriture, jamais embarqué dans du HTML cachable, envoyé dans l'en-tête X-OWC-Nonce. La récupération est plafonnée à 4 secondes, et un nonce absent, vide ou injoignable n'empêche jamais l'écriture ni n'est rapporté au visiteur.
  3. page_url explicite dans le corps de la requête, pour que la ligne de registre ne dépende pas de l'en-tête Referer qu'une extension, une meta-referrer ou un proxy peut supprimer.
  4. Garde anti-blocage de 15 secondes avec AbortController : la requête est annulée, pas seulement ignorée, pour qu'un POST tardif n'écrive pas une seconde ligne.
  5. Définition du succès. Le POST est un succès si, et seulement si : la réponse est ok, le corps est du JSON analysable, json.ok === true, et le cookie est relu dans le navigateur. Une page de cache, un défi de bordure ou un WAF servi en 200 ne comptent pas.
  6. Si le serveur répond cookie_set: false, le runtime écrit lui-même le cookie avec les paramètres renvoyés, puis relit. Un échec reste un échec : rien n'est publié, rien n'est débloqué.
  7. Une seule ré-émission, et seulement sur un 403 dont le code est owc_bad_nonce, rest_cookie_invalid_nonce ou rest_nonce_invalid, et seulement si le nonce obtenu est réellement différent.
  8. Ce que le runtime publie est ce que le serveur a stocké : les catégories rejetées par le serveur sont soustraites de l'état local.

Tant que le POST n'a pas réussi, rien n'est démasqué, rien n'est débloqué, et aucun signal Consent Mode « granted » n'est émis.

La taxonomie d'erreur

Le runtime distingue neuf causes, chacune avec son message visible : network, refused, ratelimit, unexpected, timeout, browser, config, cookie, owc_cookie_not_persisted. Le statut HTTP et le code serveur sont posés sur l'élément (data-owc-status, data-owc-code) et journalisés une fois en console.warn — jamais rendus en texte visible. Une capture d'écran de support nomme donc la cause sans qu'il faille une trace réseau.

L'API JavaScript de la bannière

window.OWCBanner.show();              // ouvre en mode bannière
window.OWCBanner.hide();              // ferme
window.OWCBanner.openPreferences();   // ouvre le panneau de préférences
window.OWCBanner.openDnsmpi();        // ouvre le panneau avec marketing, social
                                      // et preferences déjà sur OFF (entrée CCPA)
window.OWCBanner.reset();             // efface le cookie côté client, poste un retrait,
                                      // puis recharge la page
window.OWCBanner.acceptCategory( 'social' );  // renvoie une promesse résolue à true
                                              // seulement si le serveur a bien stocké

Un bootstrap plus léger est imprimé dans <head> en priorité 1 et expose window.OWConsent : .config, .categories, .geo, .profile, .optOut, .state, .has(cat), .refresh(), .paint(), .gcmSignals(state). Sa configuration est filtrable par owc_bootstrap_config — mais tout ce qui s'y trouve est public et partagé par le cache : n'y mettez jamais de donnée propre à un visiteur.

Ouvrir le panneau depuis vos pages

Trois manières, toutes équivalentes :

  • le fragment d'URL #owc-preferences (ou #owc-dnsmpi), pris en compte au chargement et sur hashchange ;
  • tout élément portant la classe owc-open-preferences ou l'attribut data-owc-open ;
  • l'attribut data-owc-dnsmpi="1" pour forcer le comportement « Do Not Sell ».

Un événement owc:consent-changed est diffusé à chaque changement, avec la charge utile envoyée en détail. Il est écouté par le bootstrap, le bouton flottant, le runtime CCPA et le module TCF ; vous pouvez l'écouter aussi.

Limites de la bannière

  • Tout dépend de wp_head() et wp_footer() : un thème qui ne les appelle pas n'obtient rien.
  • Sans window.fetch et window.Promise, aucune écriture n'est possible et l'erreur browser est affichée.
  • Un consentement peut être enregistré côté serveur sans être conservé côté navigateur (adresse du site différente de l'hôte parcouru, stockage bloqué, jar plein). Le runtime détecte ce cas, refuse de le compter comme un succès, affiche un message dédié et transmet le fait au serveur à l'écriture suivante.
  • La bannière n'a pas de shortcode : elle s'affiche partout ou nulle part.

Le blocage automatique

C'est le module qui transforme un choix en effet réel. Il réécrit les balises porteuses de traceurs avant tout consentement, pour tout le monde, et laisse le runtime les libérer dans le navigateur.

Deux modes, un seul réglage

compliance_strict, activé par défaut.

  • Désactivé (mode souple) : seul le filtre script_loader_tag est branché. Autrement dit, seuls les scripts enregistrés par wp_enqueue_script() sont réécrits. Un <script> écrit en dur dans le thème, un embed, une iframe, un pixel <img> : rien n'est touché.
  • Activé (mode strict) : en plus du filtre, un tampon de sortie capture tout le document, et le balisage rendu via l'API REST est traité lui aussi. En administration, si la requête est un admin-ajax.php, un tampon séparé couvre les réponses qui servent aussi le front (« charger plus », archives filtrées) — mais uniquement si le gestionnaire a lui-même déclaré Content-Type: text/html.

Ce qui n'est jamais bufferisé

Le tampon strict sort immédiatement pour : l'administration, les requêtes AJAX, les flux, robots.txt, les trackbacks, le cron, les favicons, les requêtes REST, les requêtes JSON, la prévisualisation de l'outil de personnalisation, les sitemaps, et les points d'entrée wp-login.php, wp-register.php, wp-signup.php, xmlrpc.php, wp-cron.php, wp-trackback.php — testés sur le nom exact du script, jamais en sous-chaîne de l'URL. Il sort aussi pour les requêtes du scanner, qui ont besoin du HTML brut.

Les cinq passes

La réécriture se fait par expressions régulières calibrées, jamais par un analyseur DOM. Trois garde-fous encadrent l'ensemble : un document de plus de 8 Mo est renvoyé intact, un échec du moteur PCRE renvoie le document intact, et si le résultat fait moins de la moitié de la taille d'origine, l'original est renvoyé. Autrement dit, le mode de panne du blocage est « page servie sans blocage », jamais « page cassée ».

Passe 0 — masquage. Commentaires HTML, <style> et <textarea> sont remplacés par des marqueurs à base de caractères de contrôle, pour qu'aucun motif ultérieur ne matche à l'intérieur.

Passe 1 — <script>. Décision, dans l'ordre :

  1. déjà traité → intact ;
  2. script du plugin lui-même → intact ;
  3. type="text/plain" ou type non-JS (ld+json, importmap, x-template) → intact ;
  4. URL cherchée dans src, data-src, data-rocket-src, data-lazy-src, data-litespeed-src, data-cfsrc — les attributs des plugins de performance sont donc couverts ;
  5. hôte dans la liste blanche → intact ; ressource first-party qui ne ressemble pas à un traceur → intact ;
  6. correspondance au catalogue sur la forme hôte + chemin ; une URL data: ou javascript: est décodée et jugée comme un corps en ligne ;
  7. une règle classée necessary n'est jamais bloquée (Stripe.js, reCAPTCHA, Turnstile, cdnjs…) : la bloquer ne gagne aucune conformité et casse le snippet appelant ;
  8. aucune correspondance → blocker_unknown_script_policy, défaut allow ;
  9. sinon réécriture en type="text/plain" avec data-owc-cat, data-owc-vendor, data-owc-src et, si le type d'origine était particulier (module, par exemple), data-owc-type pour ne pas dégrader le script restauré.

Les corps en ligne sont comparés d'abord aux règles de catalogue de type inline_signature, puis à quatorze signatures ancrées codées en dur : fbq(, _fbq.push, gtag(, dataLayer.push(, ga('…, _gaq.push, _paq.push, hjid, clarity(, ttq.load|track|page, snaptr(, twq(, lintrk(, pintrk(. Exception explicite : un gtag('consent', …) seul, c'est-à-dire une déclaration Consent Mode par défaut sans autre marqueur, reste exécuté.

Passe 2 — <iframe>. Mêmes attributs d'URL, plus data-original et data-srcset. First-party ou liste blanche → intact. necessary → intact, sinon un formulaire à captcha deviendrait insoumettable. Sans correspondance → blocker_unknown_iframe_policy, défaut block. L'iframe bloquée devient src="about:blank" et est enveloppée dans un substitut visuel contenant un bouton « Accepter » qui débloque la catégorie correspondante.

Passe 3 — <img>, pixels de tracking uniquement. Règle importante : une ligne de catalogue seule ne suffit jamais à neutraliser une image. Si l'image correspond au catalogue mais que sa catégorie n'est ni analytics ni marketing — un CDN d'images, un service de gravatars, un hébergeur de polices — elle est laissée intacte, sinon on supprimerait les médias du site au lieu d'un traceur. Seule l'heuristique de balise peut bloquer : onze points de collecte connus (facebook.com/tr, px.ads.linkedin.com, ct.pinterest.com, bat.bing.com, analytics.twitter.com, t.co/i/adsct, tr.snapchat.com, analytics.tiktok.com, google-analytics.com/collect, googleads.g.doubleclick.net, pixel.quantserve.com) ou une image tierce de 1×1 pixel. Le src est alors remplacé par un GIF transparent, et tous les attributs porteurs d'URL sont supprimés pour qu'un chargeur paresseux ne restaure pas l'original.

Passe 4 — <link>. Conditionnée à blocker_block_resource_hints, défaut true. Ne traite que les hôtes déjà présents au catalogue, jamais un inconnu.

  • Les indices de ressources (preconnect, dns-prefetch, prefetch, prerender, preload, modulepreload) vers un tiers catalogué sont supprimés, pas différés : un indice ouvre une connexion TCP+TLS et divulgue l'IP et l'empreinte TLS du visiteur, et il n'y a rien à restaurer ensuite.
  • Une feuille de style classée analytics ou marketing est neutralisée.
  • Google Fonts et les autres hôtes de typographie classés functional ne sont jamais touchés : les neutraliser donne un texte en police de repli sur tout le site, pour aucun gain de conformité.

Passe 5 — <object>, <embed>, <source>, <video>, <audio>. Jamais de blocage par défaut : seul un tiers déjà catalogué est traité. Les attributs autoplay et preload sont retirés.

Ce qui compte comme « first party »

L'URL du site, l'URL WordPress, l'URL de contenu, l'URL des includes, leurs équivalents réseau en multisite, et la base des téléversements. Le www. est retiré à la comparaison. Tout chemin commençant par /wp-content/ ou /wp-includes/ est first-party quel que soit l'hôte, pour couvrir les réécritures CDN. Les schémas non-HTTP (data:, blob:, javascript:) ne sont jamais first-party.

Contre-exception : une URL contenant gtag, gtm.js, analytics, pixel, fbevents, hotjar, matomo, piwik ou clarity est traitée même sur l'hôte du site. C'est ce qui attrape un GTM ou un Matomo auto-hébergé, et les proxys first-party.

Le runtime de déblocage

Imprimé dans <head> en priorité 2. Sa configuration JSON ne contient que le nom du cookie et la liste d'hôtes surveillés — rien qui dépende du visiteur.

  • Lecture d'état : le cookie owc_consent est décodé jusqu'à stabilité, avec un maximum de trois passes, parce que les cookies écrits avant la 1.4.3 étaient encodés deux fois.
  • Garde des scripts injectés dynamiquement : le setter HTMLScriptElement.prototype.src et setAttribute sont enveloppés. Un chargeur first-party qui affecte s.src = 'https://www.googletagmanager.com/gtm.js?id=…' est intercepté et l'élément est estampillé avant exécution. La liste surveillée compte vingt hôtes : googletagmanager.com, google-analytics.com, googleadservices.com, googlesyndication.com, doubleclick.net, connect.facebook.net, static.hotjar.com, script.hotjar.com, clarity.ms, cdn.matomo.cloud, analytics.tiktok.com, snap.licdn.com, sc-static.net, static.ads-twitter.com, bat.bing.com, s.pinimg.com, cdn.segment.com, js.hs-scripts.com, cdn.amplitude.com, cdn.mxpnl.com. Tout ce qui n'y figure pas passe sans être inquiété. Les hôtes de votre liste blanche sont retirés de cette liste avant impression.
  • Réinjection en ordre du document. Un script externe restauré bloque la file jusqu'à son onload ou onerror, avec un délai maximum de 5 secondes pour qu'un vendeur injoignable ne bloque pas le reste. L'attribut async n'est appliqué que s'il était présent d'origine — sans cela, un script créé par createElement serait forcé en asynchrone et le snippet de configuration s'exécuterait avant sa bibliothèque. Pour un script en ligne restauré, document.write est temporairement redirigé pour ne pas effacer le document.
  • Retrait de consentement : si une catégorie déjà appliquée passe explicitement à false, le runtime recharge la page. Un script déjà exécuté ne peut pas être déchargé (art. 7.3 RGPD). Une catégorie simplement absente de la charge utile n'est pas un retrait.
  • Un MutationObserver re-scanne le balisage injecté après chargement (AJAX, sections paresseuses).
  • Un clic sur le bouton d'un embed bloqué appelle OWCBanner.acceptCategory() ; si l'API de la bannière n'existe pas, l'embed est débloqué localement, sans rien persister.

Le catalogue de signatures

Table {prefix}owc_scripts. Le fichier livré, data/tracker-catalog.json, contient 175 lignes couvrant 71 vendeurs distincts :

RépartitionDétail
Par cible110 motifs d'URL, 65 noms de cookies
Par type172 fragments de texte, 3 expressions régulières
Par catégorienecessary 55, marketing 37, functional 37, analytics 36, social 10

Colonnes : pattern, pattern_type (host, regex, inline_signature), match_target (url ou cookie), name, vendor, category, privacy_policy_url, gcm_signal, tcf_vendor_id, retention_days.

Deux comportements à connaître :

  • Les lignes match_target = 'cookie' ne sont jamais utilisées par le blocage. Elles servent uniquement à la table des cookies des documents générés. Les comparer en sous-chaîne d'URL est précisément ce qui cassait le JavaScript des sites.
  • Un motif de type host visant une URL, de moins de six caractères et sans point, est refusé : « fr » ou « IDE » matcheraient « frame.js » et « provider.js ».

Le catalogue est mis en cache dans le cache objet (1 heure) et dans un transient (12 heures). Un résultat vide n'est jamais mis en cache. Toute écriture déclenche l'action owc_catalog_updated, qui vide ces caches.

L'onglet Tracker catalogue permet d'ajouter, modifier, supprimer et rechercher des règles, avec un filtre « seulement les règles qui ne peuvent pas se déclencher » : motif vide ou invisible, expression régulière qui ne compile pas ou qui rétro-suit de façon catastrophique, motif trop court et sans point pour une URL, catégorie qui n'existe plus. Ce diagnostic est fait en PHP, parce que seul le moteur PCRE peut dire si une expression compile.

Réglages du blocage

RéglageDéfautValeurs
compliance_stricttruebooléen
blocker_unknown_script_policyallowallow, block
blocker_unknown_iframe_policyblockallow, block
blocker_block_resource_hintstruebooléen
blocker_allowlist''un hôte par ligne

Points d'extension

// Hôtes jamais bloqués, en plus de blocker_allowlist.
add_filter( 'owc_blocker_allowlist', function ( array $hosts ) {
    $hosts[] = 'cdn.mon-partenaire.example';
    return $hosts;
} );

// Lignes du catalogue avant validation.
add_filter( 'owc_scripts_catalog', function ( array $rows ) {
    $rows[] = array(
        'pattern'      => 'tracker.example.com',
        'pattern_type' => 'host',
        'match_target' => 'url',
        'name'         => 'Exemple',
        'vendor'       => 'Exemple SA',
        'category'     => 'analytics',
    );
    return $rows;
} );

La méthode publique OWC_Blocker::block_html_fragment( $html ) applique le blocage à un fragment qui n'est pas passé par le tampon de sortie.

Limites du blocage

  1. Sans compliance_strict, seuls les scripts mis en file par WordPress sont traités.
  2. Un script tiers inconnu est autorisé par défaut. Le blocage réel repose sur le catalogue et les signatures en ligne — enrichissez-les.
  3. La garde des scripts injectés par JavaScript ne couvre que vingt hôtes ; un traceur hors liste injecté par du code first-party passe.
  4. Les feuilles de style classées functional (polices web) sont volontairement laissées passer.
  5. Un traceur catalogué en <img> mais classé autrement qu'analytics ou marketing n'est pas neutralisé.
  6. Les médias (object, embed, source, video, audio) ne sont jamais bloqués par défaut : seul un tiers déjà catalogué est traité.
  7. Un document de plus de 8 Mo, un échec PCRE ou une perte de plus de 50 % du contenu donnent une page servie sans aucun blocage, silencieusement.
  8. Le retrait de consentement provoque un rechargement complet de la page.

Le scanner de traceurs

Le scanner inventorie ce que vos pages chargent réellement. Il est désactivé par défaut : activez-le dans l'onglet Scanner.

Trois sources de détection

Chaque trouvaille conserve sa provenance et l'information « le blocage peut-il agir dessus ».

1. Le HTML servi. Le scanner récupère un échantillon des URLs de votre propre site et lit le balisage : <script src> et <script data-owc-src> — les scripts déjà bloqués sont donc vus quand même —, signatures en ligne, <iframe>, feuilles de style externes et images dont l'URL contient 1x1, pixel, track, beacon ou impression. Aucun JavaScript n'est exécuté : ce qu'un gestionnaire de balises injecte au runtime est invisible pour cette passe.

2. Les en-têtes Set-Cookie. Ce sont les seuls cookies que le balayage serveur peut prouver, y compris les cookies HttpOnly qu'une sonde navigateur ne verra jamais. Ils sont marqués non blocables : rien dans le blocage client ne peut retenir un cookie émis par le serveur. Les cookies du plugin lui-même sont ignorés.

3. La sonde navigateur. Imprimée en pied de page uniquement pour un utilisateur connecté disposant de manage_options, et seulement si le scanner est activé. Elle photographie document.cookie au chargement, observe les mutations du DOM — c'est ce qui attrape ce qu'un gestionnaire de balises injecte —, interroge document.cookie toutes les 5 secondes pour les cookies écrits côté client, et transmet ses observations toutes les 2 secondes et sur beforeunload. La réponse portant la sonde est marquée non cachable.

Limite reconnue dans le code : un administrateur a en général déjà tout accepté. La sonde décrit donc l'état post-consentement, pas l'état pré-consentement.

Le balayage

La file d'URLs est construite pour couvrir des gabarits, pas des pages : les URLs cœur d'abord (accueil, page d'accueil statique, page des articles, et les quatre pages de politiques liées), puis un entrelacement en tourniquet par famille — jusqu'à 30 pages, 15 articles, 5 par type de contenu public personnalisé, 3 catégories et 3 étiquettes. Sans cet entrelacement, trente pages quasi identiques mangeaient tout le budget et les gabarits WooCommerce n'étaient jamais atteints.

Le filtre owc_scanner_urls permet d'ajouter des URLs, mais le résultat est re-restreint à l'hôte du site : le scanner ne sort jamais de votre domaine.

Chaque requête est faite avec les redirections suivies à la main (deux sauts maximum, chaque saut re-testé contre l'hôte du site), une réponse plafonnée à 2 Mo, un en-tête Cache-Control: no-cache, no-store et un paramètre d'URL unique pour ne pas lire une page de cache, et un user-agent OW-Consent-Scanner/<version>. Une réponse non-HTML est comptée comme ignorée, pas comme un échec.

Un secret de contournement est envoyé dans l'en-tête X-OWC-Scanner pour que le blocage se retire et laisse voir le HTML brut. Il est comparé en temps constant, et l'en-tête est testé en premier pour qu'un visiteur ordinaire ne déclenche même pas la lecture de l'option.

Budget, verrou, reprise

ContrainteValeur
URLs par exécutionscanner_max_urls, défaut 25, bornes 1 à 500
Délai par requêtescanner_timeout, défaut 8 s, bornes 1 à 60
Budget mural par lotmax_execution_time − 10 s, sinon 45 s, borné entre 5 et 60 s
Verrou d'exécution15 minutes
Durée de vie de la file6 heures

Si le budget s'épuise, une reprise est planifiée une minute plus tard et le balayage continue là où il s'était arrêté. Un second lancement pendant qu'un balayage tourne répond « un scan est déjà en cours ». Court-circuit : si trois requêtes échouent sans qu'aucune page n'ait été lue, le balayage s'arrête au lieu de brûler son délai sur vingt-deux URLs de plus — c'est le cas typique d'un site dont le loopback HTTP est bloqué.

Les signatures en ligne reconnues

Vingt-trois aiguilles, regroupées en vingt et un libellés : gtag('config', gtag('event', gtag('js', gtm.start, ga('create', ga('send', fbq('init', fbq('track', _paq.push, (h.hj=h.hj, clarity('init', clarity.ms/tag, mixpanel.init, amplitude.init, amplitude.getInstance, _linkedin_partner_id, snaptr('init', ttq.load, pintrk("load", pintrk('load', rdt('init', criteo_q.push, _etmc.push.

La clé de dédoublonnage est la signature, jamais une empreinte du corps : un vrai snippet gtag ou Pixel contient des valeurs propres à la page, ce qui donnerait une ligne par page.

La boucle de rétroaction : trouvaille → catalogue → blocage

C'est le point qui rend le scanner utile. Classer un traceur écrit une règle dans le catalogue de blocage. Un clic sur la catégorie d'une trouvaille fait trois choses : il écrit la catégorie sur la ligne, il mémorise votre décision manuelle, et il crée la règle correspondante. À la fin de chaque balayage complet, la même opération est faite en masse pour ce que le catalogue ne sait pas déjà classer.

Règles de conversion :

  • une trouvaille de type cookie devient une règle match_target = cookie sur le nom du cookie ;
  • une trouvaille de type inline_script n'écrit une règle que si l'aiguille d'origine est retrouvée. Sans elle, aucune règle n'est écrite : une règle construite à partir du libellé « Google gtag (config) » serait morte à jamais et matcherait le nom de la trouvaille, ce qui la ferait passer pour « couverte » pour toujours ;
  • sinon, une règle host sur le domaine, refusée si le domaine est vide.

Vos décisions manuelles sont stockées à part (500 au maximum) et réappliquées à chaque nouvelle observation, parce que le drapeau « confirmé » signifie aussi « une règle de catalogue a matché » et ne peut donc pas porter seul l'information « l'administrateur a tranché ». Reclasser en « non catégorisé » supprime l'entrée : c'est un vrai retour arrière.

Le catalogue est comparé motif le plus long d'abord, pour qu'une règle large (google-analytics.com) n'écrase pas la règle plus précise que vous avez écrite (www.google-analytics.com).

Planification, alertes, rétention

scanner_frequency accepte hourly, twicedaily, daily et weekly — mais l'administration ne propose que les récurrences que votre installation connaît réellement. Défaut : weekly. Désactiver le scanner déprogramme les deux événements.

Deux e-mails distincts, jamais les deux à la fois :

  1. Traceurs non catégorisés — envoyé uniquement pour des identifiants réellement nouveaux, avec une mémoire plafonnée à 500 entrées.
  2. « Le balayage n'a pas pu lire ce site » — limité à un message par semaine et par signature d'erreur. C'est le mode de panne le plus grave, parce qu'un balayage qui ne lit rien ne produit aucune trouvaille, donc aucune alerte du premier type.

Destinataire : scanner_alert_email, sinon l'adresse d'administration du site.

Rétention : 90 jours. Les trouvailles non revues depuis 90 jours sont supprimées, dès qu'au moins une page a été lue — pas seulement quand un balayage se termine.

Le panneau de couverture

C'est la partie la plus importante de l'onglet Scanner, et elle alimente aussi l'encart de mise en garde des documents générés. Elle répond toujours à ces questions :

  • un balayage a-t-il déjà été lancé ?
  • le dernier a-t-il fini, ou a-t-il été interrompu par son budget de temps ?
  • combien de pages ont été réellement récupérées (pas retirées de la file) ?
  • combien n'ont pas pu l'être, et quelle a été la première erreur ?
  • un navigateur a-t-il déjà rapporté quelque chose, ou aucun JavaScript n'a-t-il jamais été observé ?
  • quelle est la fenêtre au-delà de laquelle un traceur non revu disparaît ?

Un balayage qui n'a atteint aucune page est présenté comme un échec, pas comme un résultat propre. C'est ce panneau qu'il faut lire avant de publier une politique cookies construite sur ces trouvailles.

L'onglet Scanner

Trois cartes (traceurs détectés, non catégorisés, dernier balayage et prochaine exécution), le panneau de couverture, un bouton « lancer un scan maintenant », le formulaire de planification, puis la liste des trouvailles : filtres par catégorie, recherche texte, filtre par type d'élément (script, inline_script, iframe, stylesheet, pixel, cookie, link, preconnect), filtre par état, export CSV, pagination par 25. Classement à l'unité ou en masse — le sélecteur de masse a un premier élément vide désactivé, pour qu'une soumission accidentelle ne devienne pas un déclassement général.

Réglages du scanner

RéglageDéfautBornes
scanner_enabledfalsebooléen
scanner_frequencyweeklyhourly, twicedaily, daily, weekly
scanner_max_urls251 à 500
scanner_timeout81 à 60
scanner_probe_modeadminsadmins, off
scanner_alert_email''adresse e-mail

À savoir : scanner_probe_mode n'est pas consommé dans cette version. Le code le signale explicitement — la condition d'impression de la sonde ne lit plus ce réglage et se base uniquement sur scanner_enabled. Enregistrer off stocke la valeur sans désactiver la sonde. Pour arrêter réellement la sonde, désactivez le scanner.

Limites du scanner

  1. Aucun JavaScript n'est exécuté par le balayage serveur. Sans la sonde, ce qu'un gestionnaire de balises injecte est absent de l'inventaire.
  2. La sonde est réservée aux administrateurs connectés et il n'existe aucun mode qui l'exécute pour un visiteur ordinaire : cela exigerait sa propre information préalable.
  3. Un cookie posé par en-tête Set-Cookie est détecté mais non blocable.
  4. Le balayage ne sort jamais du domaine et est plafonné à 25 URLs par défaut : un grand site n'est jamais couvert intégralement.
  5. Sur un hôte où le loopback HTTP est bloqué (authentification HTTP de préproduction, pare-feu), le balayage ne lit rien.
  6. Une trouvaille non revue depuis 90 jours disparaît, et disparaît donc aussi de la politique cookies.
  7. Le scanner dépend de WP-Cron : sur un site avec DISABLE_WP_CRON et sans cron système, le balayage planifié ne se déclenche pas.

Le générateur de documents légaux

Les quatre documents

TypeContenuLangues disponibles
cookie_policyPolitique cookiesfrançais, anglais, portugais
privacy_policyPolitique de confidentialitéfrançais, anglais, portugais
legal_noticeMentions légalesfrançais uniquement
termsCGU / CGVfrançais uniquement

Chaque document généré est une page WordPress, versionnée par des métadonnées : _owc_policy_type, _owc_policy_version (incrémentée à chaque génération), _owc_policy_hash (empreinte SHA-256 du HTML), _owc_policy_generated_at, _owc_policy_profile, _owc_policy_lang, _owc_policy_manual_edit.

Après génération, le lien est réinjecté dans les réglages correspondants (link_cookie_policy, link_privacy_policy, link_legal_notice, link_terms), et la publication d'une politique de confidentialité met à jour la page de politique déclarée dans WordPress.

Deux verrous avant publication

Verrou 1 — les champs obligatoires. La génération est refusée avec la liste des clés vides, plutôt que de publier des clauses à trous.

DocumentChamps exigés
Touslegal_company_name, legal_company_email
Sauf la politique cookies+ legal_company_address, legal_country
Mentions légales+ legal_company_phone, legal_publication_director, legal_host_name, legal_host_address, legal_host_phone
Mentions légales, pays FR / BE / LU+ legal_company_legal_form, legal_company_reg_number

Verrou 2 — la langue. Le générateur refuse de publier un document rédigé dans une langue que la juridiction n'utilise pas. En prévisualisation, le document sort avec un bandeau rouge d'avertissement ; en publication, c'est un refus net. Dans l'onglet Policies, le bouton de génération est masqué lorsqu'aucun gabarit n'existe pour le type et le profil actifs, plutôt que de s'afficher et d'échouer systématiquement.

Résolution de la langue

ProfilLangue du document
quebecfrançais
lgpdportugais
gdpr avec legal_country ∈ {FR, BE, LU}français
Tout le resteanglais

L'avertissement obligatoire

Tant que policy_disclaimer est activé — et il l'est par défaut — chaque document se termine par un bloc qui dit que c'est un modèle généré automatiquement, qu'il doit être relu par un professionnel qualifié avant publication, et qui porte la date de génération et la version du plugin. Ne le désactivez qu'en connaissance de cause.

Le tableau des traceurs — les règles d'honnêteté

Le tableau publié dans la politique cookies vient de votre table de scanner, pas d'une base tierce. Cinq règles gouvernent ce qu'il affiche.

  1. Fenêtre de fraîcheur : seules les trouvailles revues dans les 90 derniers jours sont publiées. Si la colonne de dernière observation n'existe pas encore parce qu'une migration n'est pas passée, la fenêtre est ignorée plutôt que de publier « aucun traceur » sur un site qui en a — la sous-divulgation est la seule direction où un document légal ne doit jamais échouer.
  2. Encart de mise en garde en tête du tableau, dérivé du panneau de couverture du scanner : balayage jamais lancé, aucune page lue, balayage interrompu, N URLs en échec, aucune observation avec JavaScript actif, fenêtre de N jours. Un balayage incomplet est divulgué, pas publié comme un inventaire terminé.
  3. Les traceurs non classés ne sont pas cachés : ils ont leur propre section. Ce sont ceux que personne n'a examinés.
  4. Un cookie observé dans un en-tête Set-Cookie est marqué d'un signe distinctif, avec une note expliquant qu'il est déposé par le serveur et qu'aucun blocage client ne peut le retenir. Il ne doit donc pas être présenté comme conditionné au consentement, et la clause qui affirme que les traceurs non classés ne sont déposés qu'après consentement porte l'exception correspondante.
  5. Le nom du responsable et le lien vers sa politique viennent du catalogue, uniquement pour les règles ciblant une URL. Les motifs de nom de cookie sont exclus : on n'affirme pas dans un document légal qui traite des données sans preuve.

Les cookies du site et les durées publiées

La section « cookies déposés par ce site » liste les cookies du plugin lui-même : owc_consent (durée dérivée du réglage de renouvellement), euconsent-v2 si TCF est activé, owc_geo si la détection régionale est activée, owc_gpc (durée de session), owc_gpc_notice (5 minutes), plus les cookies de connexion et de réglages de WordPress.

Le tableau des durées de conservation tire ses chiffres des réglages réelsledger_retention_days, retention_form_data_days, retention_dsar_days et la durée de renouvellement du consentement. Aucune valeur décorative n'est publiée. La durée du cookie de consentement est dérivée du même calcul que le cookie lui-même, ce qui garantit qu'un réglage de 0 mois publie bien « 13 mois » et non « jamais ».

Détection du type d'activité (CGU/CGV)

Les CGU/CGV comptent 21 sections, avec bascule vendeur/prestataire, produits/services, devis/commande, et des clauses spécifiques par type d'activité.

legal_business_type (auto, vitrine, rental, ecommerce, services, saas, content) est la source de vérité. Quand il vaut auto, une heuristique examine le site — présence de WooCommerce, inscription ouverte, plugins d'abonnement, page de tarifs, pages de services, puis densité de mots-clés sur les cinquante derniers contenus publiés — mais son verdict n'est jamais imprimé dans un document publié : il ne sert qu'à choisir les clauses optionnelles.

Les citations d'articles français ne sont insérées que si legal_country vaut FR. La Belgique a ses propres citations (CDE art. VI.45 §1er, art. VI.47, Service de médiation pour le consommateur) ; le Luxembourg et le Québec reçoivent une formulation neutre. Citer un texte non vérifié serait le défaut, pas le remède.

Autres comportements

  • La date du document est formatée dans la locale du document. Si le pack de traduction correspondant n'est pas installé, le format bascule sur jj/mm/aaaa : un document français ne peut pas s'ouvrir sur « 4 September 2026 ».
  • Édition manuelle détectée : si le contenu stocké ne correspond plus à son empreinte, une révision est enregistrée avant écrasement et l'écran affiche la pastille « modifications manuelles remplacées », avec un lien vers les révisions.
  • Le générateur ne rétrograde jamais : une page publiée reste publiée même si la case « publier » est décochée, et un titre ou un permalien que vous avez renommé survit à une régénération.
  • Injection dans le menu de pied de page : auto_footer_menu_inject, désactivé par défaut. Le plugin ne modifie pas votre site public sans demande explicite. Emplacements reconnus quand vous l'activez : footer, footer-menu, footer_menu, footer-1, footer_1, secondary, legal.

Points d'extension

// Corps brut du gabarit, les {{variables}} encore en place.
add_filter( 'owc_policy_template', function ( $html, $type, $profile ) {
    return $html;
}, 10, 3 );

// Les variables injectées dans le gabarit.
add_filter( 'owc_policy_vars', function ( array $vars, $type, $profile ) {
    return $vars;
}, 10, 3 );

// Le HTML final, variables remplacées.
add_filter( 'owc_policy_html', function ( $html, array $vars ) {
    return $html;
}, 10, 2 );

Fournir votre propre corps via owc_policy_template désactive le verrou de langue : un site qui fournit son texte en assume la langue. C'est la voie officielle pour publier des mentions légales non françaises.

Quatre blocs pré-générés sont disponibles comme variables réservées : {{__trackers_table__}}, {{__categories_list__}}, {{__retention_table__}}, {{__jurisdictional_rights__}}. Toutes les variables sont déjà échappées dans le contexte de leur usage. Trois méthodes publiques sont réutilisables par un gabarit tiers : OWC_Policies::build_cookie_table(), build_data_retention_table() et render_first_party_cookies().

Routes REST

MéthodeCheminParamètresAccès
POST/owc/v1/policies/generatetype (requis), publish (booléen, défaut false)manage_options
GET/owc/v1/policies/previewtype (requis)manage_options

Le HTML de la prévisualisation passe par wp_kses_post() avant d'être renvoyé.

Limites du générateur

  1. Ce sont des modèles, pas un conseil juridique. Faites-les relire.
  2. Mentions légales et CGU/CGV n'existent qu'en français. Toute autre juridiction se heurte au verrou de langue, sauf si vous fournissez votre propre corps.
  3. Trois langues seulement : français, anglais, portugais.
  4. Quatre pays de l'EEE ont une autorité nommée ; ailleurs, une formulation générique.
  5. Le tableau des traceurs ne vaut que ce que vaut votre balayage — et le document le dit.
  6. La classe du générateur pèse environ 440 Ko de gabarits juridiques : elle est chargée paresseusement, uniquement quand l'un des trois points d'entrée est réellement sollicité.

Le registre de consentement

Le registre est la réponse à l'article 7.1 du RGPD : pouvoir démontrer que la personne a consenti. Il est activé par défaut.

La table

{prefix}owc_ledger, toutes les dates en UTC :

ColonneTypeContenu
idbigintClé primaire
created_atdatetimeHorodatage UTC
visitor_tokenchar(32)Jeton pseudonyme du navigateur, 32 caractères hexadécimaux
eventvarchar(20)accept_all, reject_all, save_preferences, withdraw, auto, gpc_opt_out
categoriesvarchar(255)Liste des catégories accordées
profilevarchar(20)Profil de conformité en vigueur au moment de l'action
sourcevarchar(60)banner, preferences, footer_link, api, auto
ip_pseudonymousvarchar(45)IP tronquée
ua_hashchar(64)Empreinte salée du user-agent
page_urlvarchar(500)Page où l'action a eu lieu
prev_hashvarchar(128)Empreinte de la ligne précédente
row_hashvarchar(128)Empreinte de cette ligne
policies_hashchar(64)Empreinte des documents en vigueur
banner_revisionvarchar(40)Empreinte de la bannière réellement affichée
plugin_versionvarchar(20)Version du plugin au moment de l'écriture

Les trois dernières colonnes permettent de reconstituer ce que le visiteur a vu, pas seulement ce qu'il a coché.

La chaîne

Chaque ligne est signée par HMAC sur une sérialisation canonique préfixée en longueur de son contenu et de l'empreinte de la ligne précédente. La genèse est une suite de 64 zéros. L'écriture se fait dans une transaction, avec un verrou de ligne sur la dernière entrée : c'est ce verrou qui sérialise réellement les écritures concurrentes et empêche la chaîne de bifurquer. Un verrou nommé MySQL est pris en second rempart, au mieux : s'il est refusé, l'ajout continue et une action est déclenchée pour que vous puissiez le tracer.

Les valeurs sont tronquées avant signature, pour que la valeur signée soit exactement la valeur stockée.

L'honnêteté sur l'infalsifiabilité

Le plugin détermine lui-même la provenance de sa clé de signature :

ProvenanceConditionVerdict
constantOWC_LEDGER_KEY est définieInfalsifiable
wp-configAUTH_KEY et AUTH_SALT sont définies, non vides, différentes entre elles et sans la phrase par défautInfalsifiable
databaseSinonNon infalsifiable

Dans le troisième cas, WordPress stocke les salts en base : quiconque a accès à la base peut re-signer la chaîne. Le plugin le rapporte dans le résultat de vérification et affiche un avis d'administration qui le dit en toutes lettres. Une empreinte non réversible de la clé est conservée, ce qui permet de distinguer une rotation de sels d'une réécriture.

C'est la raison pour laquelle la section Installation recommande de définir OWC_LEDGER_KEY.

La vérification

Le bouton « Verify the chain now » de l'onglet Audit ledger — ou la route GET /owc/v1/ledger/verify — parcourt toute la chaîne par lots de 500 lignes.

Trois schémas de signature sont reconnus : le schéma canonique actuel, un schéma historique antérieur, et un schéma non clé issu des toutes premières versions. Ce dernier produit une empreinte que n'importe qui détenant la base peut recalculer : il n'est jamais considéré comme valide, il est compté à part et signalé comme une rupture. Un condensé non clé n'est pas une preuve.

Ruptures rapportées : hash_mismatch, chain_break, bad_genesis, unkeyed_rows, table_emptied, tail_truncated, head_mismatch, count_mismatch.

Le résultat porte notamment : ok, total, checked, table_total, broken_at, breaks, break_count, partial, legacy_rows, unverifiable_rows, anchor, anchor_ok, key_source, tamper_evident, key_rotated.

L'ancre de tête

Une ancre est conservée hors de la table : identifiant, empreinte, nombre de lignes, horodatage. Sans elle, supprimer les lignes les plus récentes ou vider la table ne laisserait aucune trace. Une action est déclenchée à chaque déplacement de l'ancre, et le code invite explicitement à la répliquer hors base — fichier, syslog, endpoint externe — pour rendre un rejeu global détectable.

Le plugin refuse d'écraser une ancre existante et signale honnêtement qu'une ancre dérivée de la table elle-même ne rend détectables que les troncatures postérieures.

La rétention

ledger_retention_days, défaut 1825 jours (5 ans), bornes 0 à 3650. La valeur 0 signifie conservation illimitée.

L'élagage tourne sur son propre cron quotidien et n'enlève qu'un préfixe contigu : jamais de trou au milieu de la chaîne. L'empreinte de la dernière ligne supprimée est mémorisée pour que le reste demeure vérifiable. Plafond par exécution : 20 000 lignes (40 lots de 500). Un très gros retard d'élagage se résorbe donc sur plusieurs jours.

Minimisation des données

  • Adresse IP : IPv4 avec le dernier octet mis à zéro (le /24 est conservé), IPv6 tronquée au /48 avec les 80 bits restants à zéro. Le masquage se fait sur la forme binaire ; une adresse IPv6 mappant une IPv4 est traitée comme de l'IPv4.
  • User-agent : seule une empreinte salée est stockée, jamais la chaîne.
  • page_url : validée contre les hôtes du site. L'appelant fait autorité — s'il fournit la valeur, même vide, c'est la réponse ; le Referer n'est consulté que s'il n'a rien dit du tout, et il est validé de la même façon.
  • Jeton visiteur : 32 caractères hexadécimaux issus d'un générateur cryptographique, sans lien avec une identité.

L'algorithme

ledger_hash_algo accepte sha256 (défaut) et sha3-256, intersectés avec les algorithmes que votre PHP supporte réellement. Le réglage est affiché mais non modifiable dans l'administration : changer d'algorithme ferait échouer la vérification de toutes les lignes existantes. Les colonnes sont dimensionnées plus large, mais le choix réel se limite à ces deux algorithmes.

L'onglet Audit ledger

Liste paginée par 25 : identifiant, date locale et horodatage UTC brut, jeton visiteur, événement, catégories, profil, IP pseudonymisée, et le maillon de chaîne (empreinte précédente → empreinte courante, tronquées à l'affichage, valeur complète en infobulle).

Filtres : jeton visiteur (32 hexadécimaux), événement, profil, plage de dates — saisies dans le fuseau du site et comparées aux horodatages UTC réellement stockés.

Exports CSV et JSON. Le JSON porte une enveloppe (format, algorithme, provenance de la clé) pour qu'une autorité puisse re-vérifier l'extrait sans disposer du reste de la chaîne. Toutes les cellules CSV sont neutralisées contre l'injection de formules de tableur.

Consulter le registre par l'API

GET /owc/v1/ledger accepte page, per_page (1 à 200, défaut 50), visitor_token, from et to. Les trois derniers permettent de répondre à une demande d'accès (art. 15) sans parcourir toute la chaîne. Les bornes sont lues en UTC, et une date au format AAAA-MM-JJ est élargie à la journée entière. Les filtres réellement appliqués sont renvoyés dans la réponse : un filtre rejeté par les contrôles ne doit pas se lire comme « voici tout le registre ».

Ce qui se passe quand le registre refuse une écriture

C'est le comportement le plus important de tout le plugin. Si le registre est activé et que l'écriture de la ligne échoue, le cookie de consentement est annulé et la réponse est un 503. Rien n'est stocké, aucun traceur n'est libéré.

Le raisonnement est direct : un consentement qu'on ne peut pas prouver ne doit pas être revendiqué. Voir Dépannage pour la marche à suivre.

Points d'extension

add_action( 'owc_consent_updated',           function ( $categories, $event, $source ) {}, 10, 3 );
add_action( 'owc_consent_cookie_not_sent',   function ( $cookie, $event ) {}, 10, 2 );
add_action( 'owc_ledger_lock_failed',        function ( $event, $token ) {}, 10, 2 );
add_action( 'owc_ledger_write_failed',       function ( $error, $event, $token ) {}, 10, 3 );
add_action( 'owc_ledger_anchor',             function ( array $anchor ) {} );
add_action( 'owc_ledger_pruned',             function ( $deleted, $days ) {}, 10, 2 );

owc_ledger_anchor est le crochet à utiliser pour répliquer l'ancre hors de la base.

Limites du registre

  1. L'infalsifiabilité est conditionnelle, et le plugin le dit lui-même.
  2. Les lignes antérieures à la 1.2.0 portent un condensé non clé : elles ne sont jamais re-signées — la re-signature laisserait un attaquant faire re-signer un historique falsifié — et elles empêchent le verdict « chaîne intacte » tant qu'elles subsistent.
  3. La rétention est plafonnée à 20 000 lignes par exécution quotidienne.
  4. Le registre n'est pas supprimé par l'effaceur de données personnelles de WordPress : c'est une chaîne de hachage, la retirer détruirait la preuve qu'elle existe pour porter, et elle ne contient qu'un jeton pseudonyme et une IP tronquée. Un message explique ce choix à la personne concernée.

Le portail de droits (DSAR)

Le portail permet à une personne d'exercer ses droits depuis une page de votre site. Il est activé par défaut, mais il n'apparaît que là où vous placez le shortcode.

Le formulaire

[owc_dsar_form]

Attributs :

AttributDéfautRôle
typesaccess,rectification,erasure,portability,restrict,object,optoutTypes proposés, séparés par des virgules
title« Exercer mes droits sur mes données personnelles »Titre du bloc
submit_label« Envoyer ma demande »Libellé du bouton

Si dsar_enabled est désactivé, le shortcode affiche « le portail est désactivé » et la route REST répond 404 : la désactivation est bien côté serveur.

La liste que vous demandez est intersectée avec la liste que le serveur accepte : une option refusée par l'endpoint n'est jamais proposée au visiteur.

Les huit types de demandes

access, rectification, erasure, portability, restrict, object, optout, withdraw.

withdraw est volontairement retiré de ce que le formulaire propose. Retirer son consentement aux cookies est instantané dans le panneau de préférences ; le faire passer par une procédure écrite de 30 jours rendrait le retrait plus difficile que le consentement (art. 7.3 RGPD). Un lien sous le formulaire ouvre directement le panneau de préférences pour ce cas.

Le filtre owc_dsar_types est la source de vérité unique : la liste blanche du endpoint en dérive.

Le cycle complet

1. Soumission. POST /owc/v1/dsar. Trois protections serveur, dans cet ordre :

  • Piège à robots : un champ caché rempli entraîne un rejet 400 avec le message générique d'une soumission malformée — un robot n'apprend rien sur la raison de son refus. Côté navigateur, le formulaire affiche même le message de succès sans rien envoyer.
  • Attestation (art. 12.6) : la case « je confirme exercer un droit sur MES propres données personnelles » doit être cochée, et c'est vérifié côté serveur, pas seulement dans le navigateur. Elle est enregistrée avec la demande.
  • Délai minimum de 3 secondes entre l'affichage du formulaire et l'envoi, côté client.

Limites de débit : 3 par heure et par adresse IP, 3 par jour et par adresse e-mail visée — l'adresse est choisie par l'attaquant, c'est donc elle qu'il faut plafonner — et 30 par heure pour tout le site.

La ligne est insérée avec le statut pending, un jeton de 64 caractères dont seule l'empreinte SHA-256 est stockée, une date d'expiration du jeton et une échéance de réponse. L'identifiant n'est pas renvoyé : pas d'énumération, pas de fuite de volumétrie. La réponse vaut {ok, mail_sent, message}, et mail_sent reflète l'échec réel d'envoi.

Le DPO n'est pas notifié à ce stade. Sinon un appelant anonyme ferait partir deux e-mails par soumission depuis votre domaine.

2. E-mail de vérification. Le lien pointe vers votre page d'accueil avec le jeton en clair en paramètre. Validité : dsar_token_ttl_days, défaut 7 jours, bornes 1 à 90. L'e-mail porte un en-tête Reply-To mais jamais un From réécrit — réécrire l'enveloppe est ce qui casse SPF. L'adresse de réponse est dsar_email, sinon legal_dpo_email ; jamais l'adresse d'administration du site, qui ne doit pas être publiée.

3. Confirmation d'identité. Le lien ouvre une page qui n'exécute rien : la confirmation se fait par un POST protégé par un nonce. C'est ce qui empêche un scanner de liens de messagerie (Safe Links, URL Defense, prévisualisation d'inbox) de confirmer une identité à la place de la personne.

La page est un document HTML autonome, servi hors du thème, avec noindex, nofollow et des en-têtes non cachables. Les tentatives sont limitées à 30 par heure et par IP. Les réponses sont distinctes : 404 pour un lien inconnu, 200 pour un lien déjà confirmé, 410 pour un lien expiré, 403 pour un nonce périmé, 500 pour un échec d'écriture, 200 pour une confirmation.

4. Ce que la confirmation déclenche.

  • Le statut passe à verified.
  • L'échéance est recalculée à partir de la vérification. Article 12.3 : le délai court depuis que la demande est complète, pas depuis une soumission jamais confirmée.
  • Le jeton est brûlé. Son empreinte est conservée volontairement, pour qu'une personne qui rouvre son lien lise « déjà confirmé » au lieu d'un 404 lui demandant de tout recommencer. L'usage unique est garanti par le contrôle de statut.
  • Sont enregistrés : la date de confirmation, l'IP tronquée, le jeton pseudonyme du navigateur — seul pont possible entre le registre de consentement et la demande — et l'identifiant de la demande WordPress associée.
  • Une demande WordPress native est ouverte avec l'identité déjà prouvée, donc directement dans votre file Outils → Exporter / Effacer les données personnelles. Cela fait que l'exécution couvre toutes les extensions du site, pas seulement OW Consent. Correspondance : access et portability → export ; erasure → suppression ; les autres types n'ouvrent pas de demande native.
  • Le responsable est notifié à dsar_email et dsar_notify_email, avec repli sur l'adresse d'administration si aucune n'est valide.

5. Exécution. Un panneau au-dessus de l'onglet DSAR requests liste les 20 demandes vérifiées, triées par échéance, avec le nombre de jours restants ou de retard.

ActionComportement
Télécharger les données (JSON)Refus 409 si l'identité n'a jamais été confirmée. Le fichier est nommé dsar-<id>-<AAAAMMJJ>.json
Effacer les donnéesOffert uniquement pour une demande de type erasure, et seulement après confirmation d'identité. La demande est clôturée avant l'effacement, sinon le texte libre de la demande en cours survivrait à sa propre exécution
+2 moisExtension de l'art. 12.3, une seule fois, avec un motif de 500 caractères maximum. L'échéance recule de 60 jours, les rappels sont réarmés, et la personne concernée reçoit un e-mail avec la nouvelle échéance et les motifs
ClôturerStatut resolved ou rejected, note obligatoire. Impossible de réécrire une demande déjà close, et impossible d'inscrire « honorée » sur une identité jamais confirmée
Renvoyer le lienÉmet un nouveau jeton, ce qui invalide le précédent

Cinq statuts : pending, verified, resolved, rejected, expired.

L'écran ne charge jamais le jeton ni son empreinte, et n'affiche pas le nom du demandeur.

6. Surveillance du délai. Sur le cron quotidien : jusqu'à 200 demandes vérifiées parcourues par échéance, un rappel sept jours avant, un rappel au dépassement, une seule fois chacun. Un bandeau d'administration signale les demandes hors délai et celles à moins de sept jours.

7. Purge. Les soumissions jamais vérifiées sont supprimées après péremption de leur jeton, et les demandes closes sont supprimées au-delà de retention_dsar_days. La mesure part de la clôture, pas de la soumission.

Le bundle de portabilité

L'export produit un document au format ow-consent/dsar-export, avec l'horodatage, le site, le sujet, la demande et les groupes de données. Il appelle tous les exportateurs enregistrés du site, pagine jusqu'à 50 pages par exportateur et s'arrête à 20 secondes. L'effacement fait la même chose côté effaceurs, avec les mêmes bornes.

Filtre : owc_dsar_export_bundle( $bundle, $email, $id ).

Ce que l'export et l'effacement couvrent dépend donc des extensions installées sur votre site : une extension qui n'enregistre aucun de ces crochets doit être traitée à la main.

Intégration avec l'outillage natif de WordPress

  • Exportateur enregistré sous la clé ow-consent, avec deux groupes : les demandes de droits et le registre de consentement.
  • Effaceur :
    1. les soumissions jamais confirmées sont supprimées — aucune valeur probatoire, que des données personnelles ;
    2. les demandes closes voient leur texte libre et le nom effacés, la ligne minimale (date, type, statut) étant conservée comme preuve (art. 5.2) jusqu'à retention_dsar_days ;
    3. une demande encore ouverte est conservée, avec un message expliquant qu'elle doit être répondue avant d'être supprimée ;
    4. le registre de consentement n'est pas supprimé, pour la raison expliquée plus haut.
  • Le plugin alimente aussi le brouillon de politique de confidentialité de WordPress.

La mention d'information sous le formulaire

Sous le formulaire, un bloc dépliable porte la mention de l'article 13 : identité et adresse du responsable, contact vie privée, finalité et base légale (art. 6.1.c), données collectées, destinataires et durée de conservation, délai de réponse et possibilité d'extension, autorité de contrôle compétente, lien vers la politique. Filtre : owc_dsar_form_notice( $html, $context ).

L'autorité affichée vient de la dérivation décrite dans Les onze profils de conformité, et n'est retenue que si elle commence par une majuscule : lire le réglage brut nommerait l'autorité française sur un site sud-africain, indien, australien ou californien.

Sans JavaScript, le bouton d'envoi est masqué et un message propose l'adresse de contact — pour que le navigateur ne fasse jamais une soumission native qui mettrait l'adresse du demandeur dans l'URL, et donc dans les journaux de tous les serveurs et proxys traversés.

Réglages DSAR

RéglageDéfautBornes
dsar_enabledtruebooléen
dsar_email''contact public et destinataire des notifications
dsar_notify_email''destinataire supplémentaire
dsar_response_days301 à 30 — jamais plus d'un mois
dsar_token_ttl_days71 à 90
retention_dsar_days1095 (3 ans)1 à 3650

Points d'extension

add_action( 'owc_dsar_submitted',   function ( $type, $email, $message ) {}, 10, 3 );
add_action( 'owc_dsar_verified',    function ( $id, array $row ) {}, 10, 2 );
add_action( 'owc_dsar_fulfilled',   function ( $id, $what, $trace ) {}, 10, 3 );
add_action( 'owc_dsar_mail_failed', function ( $id, $email, $kind ) {}, 10, 3 );

Limites du portail

  1. L'identité repose sur un seul facteur : l'aller-retour par e-mail. Toute preuve supplémentaire au titre de l'art. 12.6 se saisit à la main dans les notes de la demande.
  2. Un envoi d'e-mail défaillant bloque le cycle : la réponse expose mail_sent: false et le formulaire affiche un message renvoyant vers l'adresse de contact, mais la demande reste pending et le délai légal ne démarre pas.
  3. Les rappels d'échéance et la purge dépendent de WP-Cron.
  4. La table des demandes est toujours supprimée à la désinstallation du plugin, quel que soit votre réglage. Exportez avant de supprimer.

L'opt-out CCPA « Do Not Sell or Share »

Sous les profils américains, la loi exige un contrôle nommé, visible, et qui exécute réellement l'opt-out.

Deux façons de le placer

Shortcode, où vous voulez :

[owc_dnsmpi]
[owc_dnsmpi label="Ne pas vendre ni partager mes informations personnelles" class="mon-lien"]
AttributDéfaut
label« Do Not Sell or Share My Personal Information »
classowc-dnsmpi

Injection automatique en pied de page : réglage ccpa_inject_footer, activé par défaut, rendu sur wp_footer en priorité 20.

Ce que fait un clic

Un clic exécute l'opt-out. Il n'ouvre pas un panneau. Le runtime récupère un nonce frais puis envoie un reject_all avec la source footer_link et toutes les catégories optionnelles à false. Le lien passe en aria-busy="true" et une zone role="status" affiche l'état : « enregistrement en cours », puis « votre opt-out a été enregistré sur ce navigateur », ou le message d'erreur.

Le code cite la raison : selon les règlements d'application de la CCPA (§7026(a)(1)), un lien qui se contente d'ouvrir un panneau n'est pas un mécanisme conforme.

Repli sans fetch ni Promise (WebView ancien, navigateur intégré) : le clic ouvre le panneau de préférences avec les catégories publicitaires déjà positionnées sur off.

La contrainte de cache

Le HTML servi est identique pour tous. Le lien est donc rendu visible si le profil configuré du site est ccpa ou us_generic ; sinon il est rendu masqué et révélé côté client pour les visiteurs dont la juridiction l'exige, d'après le cookie de détection régionale.

Si la détection régionale est désactivée et que votre profil configuré n'est pas américain, le bloc n'est carrément pas imprimé : pas de balisage mort.

Aucune donnée de visiteur ne voyage dans la configuration JavaScript de ce module — seules les listes de profils, jamais le booléen résolu.

Les ressources de ce module sont imprimées dans <head>, pas en pied de page, pour que le refus Consent Mode atteigne gtag avant qu'un gestionnaire de balises ne se déclenche. Le JavaScript est en ES5 uniquement.

Limites

  1. Le contrôle en un clic exige fetch et Promise.
  2. Le lien n'est obligatoire que sous les profils ccpa et us_generic ; sous tout autre profil, il n'est ni affiché ni révélé.
  3. La révélation côté client dépend du cookie de détection régionale, donc de JavaScript.

Google Consent Mode v2

Le module est activé par défaut (gcm_enabled).

Les sept signaux

Les sept signaux de Consent Mode v2 sont émis : ad_storage, ad_user_data, ad_personalization, analytics_storage, functionality_storage, personalization_storage et security_storage. Les six catégories du plugin y sont associées depuis une source de vérité unique, ce qui garantit que la bannière, le blocage et le bootstrap ne peuvent pas diverger. La table est donnée dans Les catégories de cookies.

Les valeurs par défaut, avant tout consentement

L'appel default est imprimé en balisage statique, et l'update est calculé dans le navigateur : c'est ce qui garde la page cachable.

SignalRégime opt-inRégime opt-out
ad_storagedeniedgranted
analytics_storagedeniedgranted
ad_user_datadeniedgranted
ad_personalizationdeniedgranted
personalization_storagedeniedgranted
functionality_storagedenied si le site expose la catégorie functional, sinon grantedgranted
security_storagegranted toujoursgranted

Les deux jeux sont imprimés dans la même page. Le runtime choisit lequel appliquer d'après le cookie de détection régionale. Sans cela, une page mise en cache épinglerait la juridiction d'un visiteur américain sur un visiteur européen.

Les deux réglages associés

RéglageDéfautEffet
gcm_ads_data_redactiontrueMasque les identifiants publicitaires tant que ad_storage est refusé
gcm_url_passthroughtrueFait transiter gclid / dclid par les URLs tant que les cookies sont refusés

À savoir

security_storage n'est jamais touché par une mise à jour : il reste accordé, comme le prévoit la spécification. Le tableau de bord affiche une carte indiquant si Consent Mode est actif.


IAB TCF v2.2

Le module TCF est désactivé par défaut, et il exige un identifiant que le plugin ne peut pas fournir. Lisez la section des limites avant de l'activer.

Activation

RéglageDéfautBornes
tcf_enabledfalsebooléen
tcf_cmp_id00 à 4095
tcf_publisher_countryFRcode ISO à 2 lettres
tcf_publisher_purposes_li[]liste de finalités
tcf_special_features[]liste de fonctionnalités spéciales

Le plafond de 4095 n'est pas arbitraire : le champ CmpId occupe 12 bits dans la chaîne TCF.

Trois cas où le module refuse d'émettre quoi que ce soit

  1. Aucun CMP ID (tcf_cmp_id < 1) : pas de __tcfapi, pas de chaîne TC, pas de cookie euconsent-v2, pas de route REST. Un avis d'administration l'explique. Le CMP ID doit vous être attribué par IAB Europe ; le plugin n'en fournit pas. Émettre une chaîne portant un CmpId 0 serait pire que ne rien émettre.
  2. CMP ID supérieur à 4095 : même refus, avec un avis dédié. Une valeur tronquée sur 12 bits désignerait un autre CMP — une usurpation d'identité.
  3. Aucune Global Vendor List en cache : le stub est imprimé mais l'API répond cmpStatus: 'error' avec une chaîne vide, plutôt que d'inventer un numéro de version de liste.

Un diagnostic est écrit dans une option (disabled, missing_cmp_id, no_gvl, active), et uniquement en contexte d'administration ou de cron, jamais sur une page publique.

La Global Vendor List

  • Source : https://vendor-list.consensu.org/v3/vendor-list.json.
  • Jamais téléchargée pendant le rendu d'une page. Un cron quotidien s'en charge, avec une première exécution cinq minutes après l'activation ; en administration avec un cache froid, une récupération unique est mise en file.
  • Requête : 5 secondes de délai maximum, 2 redirections, réponse plafonnée à 4 Mo. Un corps qui atteint le plafond est considéré comme tronqué et rejeté.
  • Seuls les champs utiles sont conservés par vendeur ; les piles sont vidées. Si la sérialisation reste trop grosse, deux paliers de troncature s'appliquent : d'abord les libellés et les URLs, puis la réduction aux seules finalités.
  • Conservation : une semaine dans un transient, avec stale-if-error — en cas d'échec, la dernière copie valide est conservée.
  • Route publique : GET /owc/v1/tcf/gvl, limitée à 10 requêtes par heure. Elle sert la copie en cache avec un ETag et un Cache-Control: public, max-age=86400, et gère les requêtes conditionnelles. Si rien n'est en cache, elle répond 503 avec un Retry-After: 300 — jamais une liste synthétique. Cette route ne déclenche jamais de requête sortante.

Où la chaîne TC est calculée

Dans le navigateur, pas en PHP. Un stub entièrement statique est imprimé dans <head> en priorité 0 : iframe locateur, implémentation de window.__tcfapi, relais postMessage, et un encodeur de segment Core en JavaScript. La configuration publiée ne contient aucune donnée de visiteur — c'est ce qui permet de rester compatible avec un cache de page. Filtre : owc_tcf_stub_config.

Un miroir PHP de l'encodeur existe mais n'a aucun appelant dans le plugin : il est réservé aux intégrations et aux tests, avec l'avertissement de ne jamais l'imprimer dans du HTML cachable.

Ce qui est encodé

Segment Core uniquement, en base64url sans remplissage. La section vendeurs est encodée soit en champ de bits, soit en plages : les deux tailles sont mesurées et la plus petite gagne. L'index vendeurs transmis au navigateur utilise un format compact maison, avec un plafond dur au-delà duquel l'index est vidé (les consentements vendeurs sont alors perdus, la version de liste est conservée).

Correspondance catégories → finalités TCF

CatégorieFinalités
necessaryaucune — hors périmètre TCF, ce qui garantit que « Tout refuser » ne peut pas produire un consentement pour la finalité 1
functional1
analytics1, 8, 9, 10
marketing1, 2, 3, 4, 7
preferences1, 5, 6, 11
social1

Filtre : owc_tcf_purpose_map.

Intérêt légitime de l'éditeur : seules les finalités 2, 7, 8, 9, 10 et 11 sont retenues, parce que TCF v2.2 interdit l'intérêt légitime pour les finalités 1, 3, 4, 5 et 6. Toute autre valeur de tcf_publisher_purposes_li est silencieusement écartée.

Un vendeur est consenti dès qu'au moins une des finalités qu'il déclare sous base « consentement » est accordée.

Les fonctionnalités spéciales

Deux entrées seulement sont supportées : 1 — usage de données de géolocalisation précises et 2 — analyse active des caractéristiques de l'appareil. Déclarez celles que vous utilisez dans tcf_special_features ; la liste est ramenée à ce catalogue.

Elles apparaissent comme de vraies cases à cocher dans le panneau de préférences. « Tout accepter » ne les coche pas : elles exigent un opt-in explicite propre. « Tout refuser » et « Do Not Sell » les décochent. Elles vivent dans la chaîne TC elle-même, et sont réhydratées à la réouverture du panneau depuis l'API TCF.

La couche TCF dans le panneau

Elle n'est rendue que si le module est actif, c'est-à-dire tcf_enabled et un CMP ID utilisable. Deux blocs :

  • Finalités : uniquement celles atteignables via une catégorie non forcée et nommées dans le catalogue de la liste vendeurs. Un interrupteur qui reviendrait tout seul serait pire que pas d'interrupteur. Basculer une finalité écrit dans toutes les catégories qui la déclarent, puis l'affichage est re-dérivé.
  • Fonctionnalités spéciales déclarées.

Rien n'est encodé dans la chaîne qui n'ait pas été affiché.

L'API JavaScript

window.__tcfapi( command, version, callback, parameter );
// commandes : ping, getTCData, getInAppTCData,
//             addEventListener, removeEventListener, getVendorList

window.__owcTcfUpdateState( tcData );        // remplace les données et notifie les auditeurs
window.__owcTcfRefresh();                    // recalcule et émet 'useractioncomplete'
window.__owcTcfUiShown(); window.__owcTcfUiHidden();
window.__owcTcfSetSpecialFeatures( [ 1, 2 ] );

Versions acceptées : absente, null, 2, '2', 2.2, '2.2'. Toute autre valeur donne callback(null, false).

gdprApplies

Décidé côté client. Si la détection régionale est désactivée, la valeur est true. Sinon, le pays du visiteur est comparé à la liste des 31 codes où le RGPD s'applique au sens TCF (27 pays de l'UE, plus IS, LI, NO, GB et CH). Échec fermé : un pays inconnu donne true.

Le cookie euconsent-v2

Écrit uniquement côté client, et uniquement quand le module est prêt et que le visiteur a agi. Sinon le cookie est effacé. Sa durée est celle du consentement, plafonnée à 13 mois (recommandation IAB / CNIL). SameSite=Lax, Secure en HTTPS.

Limites du module TCF — à lire absolument

  1. Ce n'est pas un CMP enregistré auprès d'IAB Europe. Il exige votre propre CMP ID, et même avec lui, la politique TCF impose à un CMP enregistré de présenter des choix au niveau finalité et au niveau vendeur. Ici, les interrupteurs de finalités suivent les catégories et il n'y a aucun choix au niveau vendeur : les vendeurs sont fondés à rejeter ce signal. L'écran d'administration le dit et parle de « mode compatible CMP (non officiel) », accepté par la plupart des SSP en développement mais rejeté en production réelle dans l'EEE. Si un revenu publicitaire sous TCF compte pour vous, utilisez un CMP certifié.
  2. Segment Core uniquement : pas de segment disclosedVendors, allowedVendors ni publisherTC.
  3. Aucune restriction éditeur n'est émise : le compteur correspondant vaut toujours 0 et l'objet des restrictions est vide.
  4. purposeOneTreatment et useNonStandardTexts valent toujours false, isServiceSpecific vaut toujours true — il n'y a pas de portée globale — et l'écran de consentement vaut toujours 0.
  5. getVendorList ignore le paramètre de version et renvoie toujours la liste en cache.
  6. Les consentements éditeur recopient les finalités générales : pas de finalité personnalisée.
  7. Deux fonctionnalités spéciales seulement.
  8. Le module exige un cron fonctionnel et un HTTPS sortant. Sans liste en cache, l'API répond cmpStatus: 'error' et la route publique renvoie 503.

Global Privacy Control

GPC est un signal envoyé par le navigateur — l'en-tête Sec-GPC: 1 et la propriété navigator.globalPrivacyControl. Le plugin l'honore par défaut (gpc_honor), mais son traitement dépend du régime juridique, et c'est le point important.

Sous les profils américains : contraignant

Le signal est traité comme un opt-out universel contraignant sous les profils ccpa et us_generic. L'Australie est explicitement exclue : régime opt-out, mais qui ne reconnaît pas GPC.

L'écriture serveur ne se produit qu'après une chaîne complète de vérifications, dans cet ordre :

  1. gpc_honor est activé ;
  2. le signal est présent — l'en-tête Sec-GPC, avec un repli sur X-Sec-GPC parce que certains proxys et CDN renomment l'en-tête ; seule la valeur exacte 1 compte ;
  3. le profil effectif est ccpa ou us_genericsinon rien n'est écrit ;
  4. c'est une simple consultation de page : pas d'administration, pas de cron, pas d'AJAX, pas de REST, pas de XML-RPC, pas de WP-CLI, et la méthode est GET ;
  5. les en-têtes n'ont pas encore été envoyés — un cookie qu'on ne peut pas poser est une décision qu'on ré-enregistrerait à chaque requête ;
  6. le marqueur de session owc_gpc est absent : une fois par session de navigation ;
  7. le visiteur ne ressemble pas à un robot (user-agent vide, ou contenant bot, crawl, spider, slurp, monitor, uptime, pingdom, headless, preview, curl/, wget, python-, java/, go-http, okhttp, httpclient, libwww, facebookexternalhit) ;
  8. le choix déjà stocké ne satisfait pas déjà le signal — sinon on marque seulement le navigateur ;
  9. la limite de débit partagée est respectée : 30 par heure et par IP ;
  10. le navigateur est marqué avant l'écriture, pour qu'un échec ne devienne pas une boucle de rejeu.

L'événement est enregistré comme gpc_opt_out avec la source auto. L'URL de page est reconstruite à partir de l'URL du site et du chemin demandé — jamais de l'en-tête Host, jamais du Referer.

Portée : toutes les catégories optionnelles passent à false ; les catégories forcées restent accordées. C'est une lecture large de la notion de vente ou de partage.

Transparence : si GPC écrase un choix explicitement enregistré, un cookie de cinq minutes déclenche côté client un avis en bas d'écran qui l'explique, avec un bouton « gérer mes préférences » qui ouvre le panneau, et un bouton de fermeture.

Sous les profils RGPD et assimilés : simple indice

Hors des régimes opt-out américains, le signal est traité comme une indication, jamais comme un consentement :

  • l'attribut data-owc-gpc="1" est posé sur <html> ;
  • Consent Mode passe ad_storage, analytics_storage, ad_user_data, ad_personalization et personalization_storage à denied ;
  • toutes les catégories optionnelles sont mises à false en mémoire seulement ;
  • given reste faux, la bannière reste affichée, et rien n'est enregistré.

Un drapeau interne empêche le runtime d'annoncer au visiteur qu'il est « déjà opt-out » alors que ni le cookie ni le registre ne le disent. La justification est écrite dans le code : GPC n'est pas un signal légalement reconnu sous le RGPD et ePrivacy, la nLPD suisse, la LGPD, POPIA, PIPL, DPDP ni la Loi 25.

Les deux cookies posés par ce module

CookieDuréeRôle
owc_gpcsessionMarque que le signal a déjà été traité pour ce navigateur
owc_gpc_notice300 sDéclenche l'avis « GPC a remplacé votre choix »

Les deux sont sur path=/, SameSite=Lax, Secure en HTTPS, et lisibles par script.

Point d'extension

add_action( 'owc_gpc_honored', function ( array $categories ) {} );

La détection régionale

La détection régionale applique à chaque visiteur le profil de sa juridiction plutôt que le profil configuré du site. Elle est désactivée par défaut.

RéglageDéfaut
geo_enabledfalse
geo_default_profilegdpr
geo_mmdb_enabledfalse

La cascade de détection

Premier succès gagnant :

  1. Cloudflare (CF-IPCountry) — seulement si le site a déclaré être derrière Cloudflare ;
  2. AWS CloudFront (CloudFront-Viewer-Country) — même condition ;
  3. GEOIP_COUNTRY_CODE, la variable écrite par le serveur lui-même (mod_geoip, ngx_http_geoip), donc non forgeable par le client ; puis, si un proxy est déclaré, les variantes en en-tête HTTP ;
  4. Base MaxMind locale, si geo_mmdb_enabled est activé ;
  5. Rien.

Sans déclaration explicite dans wp-config.php, aucun en-tête HTTP de pays n'est cru : ils sont forgeables par le client. Voir Installation pour les constantes.

Le code de pays est validé contre la liste blanche ISO-3166-1 alpha-2 officiellement assignée (environ 249 codes énumérés en dur). Les codes utilisateur ou réservés — XX, ZZ, le T1 des sorties Tor — sont rejetés.

L'échec fermé

Sans pays détecté, le plugin lit geo_default_profile mais rétrograde vers gdpr tout profil opt-out. Un geo_default_profile réglé sur ccpa, us_generic ou au est donc ignoré pour les visiteurs non détectés — et l'écran d'administration ne propose que des profils opt-in dans ce sélecteur, plutôt que d'offrir un choix silencieusement ignoré.

Le motif est direct : sans ce garde-fou, un site configuré en anglais américain placerait tout visiteur de l'EEE en régime opt-out, avec des traceurs signalés « accordés » sans aucun consentement.

Aucun repli sur la locale du site : la langue d'un site ne dit rien du lieu de son visiteur.

Le cookie owc_geo

  • Durée 24 heures, path=/, SameSite=Lax, Secure en HTTPS, lisible par script — le bootstrap front en a besoin.
  • Contenu : le pays, le profil, un horodatage et un code d'authentification. Jamais d'adresse IP.
  • À la lecture : taille bornée, pays validé contre la liste ISO, horodatage dans la fenêtre, code vérifié en temps constant. Le profil est toujours recalculé côté serveur à partir du pays : un visiteur ne peut pas choisir son régime juridique.
  • Seule une détection réelle est mise en cache. Le repli « échec fermé » n'est jamais mémorisé.
  • Écrit uniquement sur une requête front-end, et seulement si la détection est activée.

La correspondance pays → profil

PaysProfil
Les 30 pays de l'EEEgdpr
GB, JE, GG, IMuk_pecr
CHch_nfadp
BRlgpd
ZApopia
CN, HKpipl
INdpdp
AU, NZau
CAquebec
USccpa
Tout le resteLe repli, rétrogradé en opt-in

Hong Kong relève en réalité de sa propre ordonnance locale ; il est traité comme PIPL par sévérité. La Nouvelle-Zélande est traitée comme l'Australie. Le Canada entier reçoit quebec — c'est le régime le plus strict qui gagne. Les États-Unis reçoivent ccpa, qui tient lieu de profil pour les autres États.

La contrainte de cache — ce que la détection ne fait pas

Le profil géolocalisé ne change pas le HTML rendu. La réécriture du profil ne s'applique qu'aux deux routes REST visiteur, /owc/v1/consent et /owc/v1/state. Le rendu de page, les routes d'administration, wp-admin, le cron et WP-CLI gardent le profil configuré. Toute réponse ayant consommé la réécriture est marquée non cachable.

C'est le runtime front qui choisit le régime, côté client, depuis le cookie. Conséquence assumée : le régime appliqué à un visiteur dépend de JavaScript, et un visiteur dont la toute première page vient du cache est traité sous le profil configuré du site tant que le cookie n'existe pas.

Le lecteur MaxMind embarqué

Le plugin embarque son propre lecteur MMDB, écrit en PHP pur, sans dépendance Composer — un plugin publié sur WordPress.org ne peut pas embarquer le paquet officiel.

  • Lecture seule, pays uniquement. Pas de ville, pas d'ASN.
  • Entrées/sorties bornées : lecture par petits blocs, jamais de chargement en mémoire d'un fichier de plusieurs mégaoctets.
  • Ne lève jamais d'exception : fichier manquant, illisible, tronqué, corrompu ou hostile donne null, et la détection retombe sur son profil strict.
  • Garde-fous internes sur la profondeur de parcours, le budget de décodage et la taille de charge utile. Les métadonnées sont mises en cache dans un transient dont la clé intègre la taille et la date du fichier : remplacer votre téléchargement mensuel invalide le cache tout seul.
  • registered_country et represented_country sont délibérément ignorés. C'est la forme documentée des plages proxy anonymes, VPN et satellite, où le pays d'enregistrement désigne le lieu d'enregistrement du bloc par le fournisseur, pas le visiteur. Les utiliser placerait un visiteur de l'EEE derrière un VPN américain en régime opt-out.
  • Si une autre extension charge déjà un lecteur GeoIP2, il est utilisé en repli après le lecteur maison.

Où mettre le fichier : wp-content/uploads/ow-geoip/GeoLite2-Country.mmdb. Le chemin est filtrable par owc_geo_mmdb_path, avec validation contre la traversée de répertoires.

Limite importante : le plugin ne télécharge pas la base et n'offre aucun écran de téléversement. Vous devez obtenir le fichier chez MaxMind, le déposer vous-même et le mettre à jour vous-même. En contrepartie, rien n'est envoyé à MaxMind : la recherche est intégralement locale, l'adresse IP sert au seul point de recherche et est explicitement détruite ensuite — jamais journalisée, jamais stockée, jamais écrite dans le cookie.

Limites de la détection régionale

  1. Désactivée par défaut, et la variante MaxMind exige un fichier que vous fournissez.
  2. Les en-têtes de pays sont ignorés tant que vous n'avez pas déclaré votre proxy.
  3. Pas de granularité état par état aux États-Unis, ni province par province au Canada.
  4. Le régime visiteur dépend de JavaScript et du cookie de 24 heures.
  5. Le profil us_generic n'est jamais attribué automatiquement.

L'intégration OW Forms

Périmètre exact : cette intégration cible le plugin frère OW Forms, et rien d'autre. Il n'y a aucune intégration Contact Form 7, Gravity Forms ou WPForms.

Réglage : forms_integration_enabled, activé par défaut, mais l'intégration ne fait rien si OW Forms n'est pas actif.

Ce qu'elle apporte

1. La liaison consentement ↔ soumission. À chaque soumission stockée, une ligne est écrite dans une table dédiée à OW Consent — le schéma d'OW Forms n'est jamais touché — contenant :

  • le jeton pseudonyme du visiteur (32 hexadécimaux, jamais l'adresse e-mail) ;
  • l'identifiant de l'entrée de registre la plus récente pour ce jeton ;
  • les catégories accordées et le profil de conformité en vigueur ;
  • l'empreinte complète des documents publiés à cet instant ;
  • l'horodatage du consentement ;
  • l'état de la case RGPD du formulaire et le libellé exact qu'elle portait.

Ce dernier point est le plus utile en pratique : vous pouvez démontrer non seulement que la case était cochée, mais ce qui était écrit à côté.

2. La cascade d'effacement. Une demande d'effacement vérifiée délègue la suppression à la routine d'OW Forms — qui supprime aussi les fichiers téléversés — puis supprime les lignes de liaison. L'intégration détecte si OW Forms a déjà branché son propre écouteur sur la même action, auquel cas elle se contente d'élaguer ses liaisons.

3. Les outils de confidentialité de WordPress. Un exportateur et un effaceur sont enregistrés, de sorte que Outils → Exporter / Effacer les données personnelles couvre les soumissions OW Forms — et le portail de droits d'OW Consent aussi, puisqu'il parcourt les mêmes crochets. L'export reprend les libellés réels des champs et ajoute les champs de consentement. Toute valeur exportée est neutralisée contre l'injection de formules de tableur et tronquée.

4. La rétention. Sur le cron quotidien, les liaisons plus anciennes que retention_form_data_days (défaut 1095 jours, bornes 1 à 3650) sont supprimées par lots de 500, avec un balayage des orphelines. Seules les lignes d'OW Consent sont touchées : la rétention des soumissions elles-mêmes appartient à OW Forms.

Limites

  1. Aucun événement de soumission n'est écrit dans le registre chaîné. La ligne de liaison référence une entrée existante, elle n'en crée pas — la chaîne ne doit pas être écrite depuis l'extérieur.
  2. Impossible de lier une soumission si OW Forms est configuré pour ne pas stocker les soumissions : il n'y a aucune ligne à laquelle s'accrocher.
  3. Aucune récupération rétroactive : la liaison commence à la première soumission après la mise à jour.
  4. Le rapprochement se fait sur l'empreinte salée de l'adresse calculée par OW Forms, avec repli sur l'adresse en clair qu'OW Forms conserve aussi. Ce n'est pas un schéma à divulgation nulle : l'adresse reste en clair chez OW Forms, parce qu'il faut pouvoir répondre à la personne.
  5. L'intégration ne modifie jamais le plugin OW Forms, ses tables ni ses options.
  6. Si OW Forms est actif mais trop ancien pour exposer sa routine d'effacement, l'effaceur répond explicitement que les soumissions n'ont pas été touchées.

Le bouton flottant

Un petit bouton persistant permet de rouvrir le panneau de préférences à tout moment. C'est l'exigence de l'article 7(3) — le retrait doit rester aussi simple que le consentement — et de la délibération CNIL 2020-091, qui demande un mécanisme accessible depuis chaque page.

Conditions d'existence

Il exige les deux réglages : floating_button_enabled et banner_enabled. Le panneau de préférences et l'API JavaScript vivent dans le runtime de la bannière ; sans elle, le bouton serait un contrôle qui ne fait rien.

Il sort aussi en administration, sur un flux, sur robots.txt, et si wp_head n'a pas été déclenché.

Options

RéglageValeursDéfaut
floating_button_enabledbooléentrue
floating_button_positionbottom-left, bottom-right, top-left, top-rightbottom-left
floating_button_stylepill, iconpill
floating_button_labeltexte libreCookies

Le bouton n'a pas de réglage de thème propre : il reprend banner_style, pour que les deux surfaces suivent la même palette.

Comportement et accessibilité

C'est un vrai <button>, donc accessible au clavier. Il porte aria-haspopup="dialog" et un aria-label traduisible mais non paramétrable (« Manage my cookie choices ») : seul le libellé visible se règle. En mode icon, le libellé est masqué visuellement mais reste lu par les lecteurs d'écran.

Il est rendu avec l'attribut hidden ; c'est le JavaScript qui décide de l'afficher. Il est visible dans tous les états sauf quand la bannière ou le panneau est déjà à l'écran — y compris immédiatement après un choix sur la page courante.

Un clic ouvre le panneau de préférences. Si l'API de la bannière n'existe pas, le bouton retombe sur le fragment #owc-preferences, en re-déclenchant l'événement à la main si c'est déjà le fragment courant.

z-index 99990, masqué à l'impression, prise en charge du mode contrastes forcés et de prefers-reduced-motion, décalage sous la barre d'administration en position haute.


Shortcodes

Le plugin déclare deux shortcodes, et deux seulement. Ni la bannière ni le bouton flottant n'en ont : ils s'affichent partout ou nulle part, pilotés par leurs réglages.

[owc_dsar_form] — le portail de droits

[owc_dsar_form]
[owc_dsar_form types="access,erasure,portability" title="Mes droits" submit_label="Envoyer"]
AttributDéfaut
typesaccess,rectification,erasure,portability,restrict,object,optout
title« Exercer mes droits sur mes données personnelles »
submit_label« Envoyer ma demande »

Les types demandés sont intersectés avec ceux que le serveur accepte. Le type withdraw n'est jamais proposé par le formulaire ; voir Le portail de droits.

[owc_dnsmpi] — l'opt-out CCPA

[owc_dnsmpi]
[owc_dnsmpi label="Ne pas vendre ni partager mes informations personnelles" class="lien-pied"]
AttributDéfaut
label« Do Not Sell or Share My Personal Information »
classowc-dnsmpi

Un clic exécute l'opt-out ; voir L'opt-out CCPA. Si vous laissez ccpa_inject_footer activé, le lien est déjà injecté en pied de page et ce shortcode n'est utile que pour le placer ailleurs.

Rouvrir le panneau depuis un menu ou un lien

Il n'y a pas de shortcode pour cela, parce qu'un attribut de classe suffit :

<a href="#owc-preferences" class="owc-open-preferences">Gérer mes cookies</a>
<button type="button" data-owc-open>Mes préférences</button>
<a href="#owc-dnsmpi" data-owc-dnsmpi="1">Ne pas vendre mes informations</a>

C'est la méthode recommandée pour ajouter une entrée « Cookies » à votre menu de pied de page.


API REST

Toutes les routes vivent sous le namespace owc/v1, à la racine REST habituelle (https://exemple.com/wp-json/owc/v1/…). Treize routes au total : douze enregistrées par le module REST, plus la route de la liste vendeurs enregistrée par le module TCF.

Routes publiques

Aucune authentification. Leur sécurité repose sur le contrôle d'origine, un jeton propre au plugin et les limites de débit décrites plus bas.

MéthodeCheminParamètresRôle
GET/nonceaction (défaut wp_rest)Renvoie un jeton frais. Réponse : {nonce, action, header: "X-OWC-Nonce", ttl: 43200}, avec no-store et Vary: Cookie
POST/consentevent (défaut save_preferences), source (défaut banner), categories (requis), page_url (défaut ''), cookie_unreadable (booléen, défaut false)Enregistre un choix, pose le cookie et écrit la ligne de registre
GET/stateL'état du seul appelant : {given, categories, profile, at}. Jamais le jeton visiteur, jamais l'IP, jamais l'URL de page. no-store + Vary: Cookie
GET/tcf/gvlSert la Global Vendor List en cache. ETag, Cache-Control: public, max-age=86400, 304 sur requête conditionnelle, 503 + Retry-After: 300 si rien n'est en cache
POST/dsartype (requis), email (requis, format e-mail), message (défaut '', 2000 caractères max)Dépose une demande d'exercice de droits. Réponse {ok, mail_sent, message}jamais l'identifiant

Valeurs acceptées :

  • event : accept_all, reject_all, save_preferences, withdraw, auto, gpc_opt_out
  • source : banner, preferences, footer_link, api, auto
  • type (DSAR) : access, rectification, erasure, portability, restrict, object, optout, withdraw
  • categories : un objet, 32 entrées maximum, valeurs scalaires uniquement
  • action (nonce) : wp_rest uniquement

Routes d'administration

Toutes exigent la capacité manage_options.

MéthodeCheminParamètresRôle
GET/ledgerpage (≥1, défaut 1), per_page (1–200, défaut 50), visitor_token (32 hex), from, toLit le registre. Les filtres réellement appliqués sont renvoyés dans la réponse
GET/ledger/verifyVérifie la chaîne de bout en bout
GET/settingsLes réglages effectifs
POST/settingscorps JSON libreÉcrit les réglages. Réponse {ok, updated, rejected, settings}
POST/scanner/runLance un balayage
POST/scanner/ingestcorps JSON {page, findings[]}Réception des observations de la sonde
GET/scanner/findingspage, per_page (1–200, défaut 50), filter (slug de catégorie ou unknown)Liste les trouvailles
POST/policies/generatetype (requis), publish (booléen, défaut false)Génère un document
GET/policies/previewtype (requis)Prévisualise un document

/scanner/ingest exige en plus un nonce wp_rest valide, dans l'en-tête X-WP-Nonce ou en paramètre _wpnonce — parce que sendBeacon ne peut pas poser d'en-tête.

/settings en écriture n'accepte que les clés déclarées comme réglages réels : un filtre peut injecter des clés synthétiques qui n'en sont pas. Chaque valeur est assainie individuellement. Aucune clé connue donne un 400.

/ledger re-teste la capacité dans le gestionnaire, en défense en profondeur contre un permission_callback filtré ailleurs.

Le modèle de sécurité des écritures publiques

Le constat de départ est simple : un nonce ne peut pas vivre dans du HTML cachable. /consent et /dsar ont donc un contrôle d'accès permissif, protégé par quatre couches dans un ordre qui compte.

  1. Contrôle d'origine même-hôte. Gratuit, déterministe, ne consomme aucun budget.
  2. Jeton propre au plugin dans l'en-tête X-OWC-Nonce — c'est une preuve, jamais un veto. Le choix de ne pas utiliser X-WP-Nonce est délibéré : le cœur de WordPress intercepte cet en-tête avant tout contrôle d'accès de route et refuse la requête entière s'il ne le valide pas. Un jeton périmé servi par un cache tuerait donc une écriture que l'endpoint aurait acceptée sans jeton du tout.
  3. Échec fermé : ni origine, ni jeton vérifié donnent un 403 owc_missing_origin.
  4. Limite de débit en dernier. Si elle passait en premier, une mauvaise configuration consommerait deux jetons par clic et finirait en 429, ce qui masquerait l'erreur de configuration derrière une limite de débit.

Hôtes acceptés : ceux que WordPress déclare (URL du site, URL WordPress, racine REST) avec leur jumeau www./apex, plus l'hôte de l'en-tête Host de la requête courante et son jumeau. « Même origine » veut dire « l'origine correspond à l'hôte auquel le navigateur s'est connecté », pas « correspond à ce que WordPress a en base » — sinon tout site joint par un domaine de prévisualisation, un alias, un nom de préproduction, un domaine mappé en multisite ou derrière un proxy qui réécrit Host serait refusé en silence, sans possibilité d'auto-réparation. Filtre : owc_allowed_request_hosts.

CORS est laissé au cœur de WordPress, délibérément.

/nonce refuse explicitement la forme JSONP : sans ce refus, une page tierce pourrait moissonner le jeton vivant d'un administrateur de passage par un simple <script src>, non soumis au contrôle d'origine.

Les limites de débit

CompartimentBudgetFenêtreClé
nonce1205 minutesadresse IP
consent305 minutesadresse IP
dsar31 heureadresse IP
dsar_email324 heuresadresse e-mail visée
dsar_global301 heuretout le site
gpc301 heureadresse IP
tcf_gvl101 heureadresse IP
  • Fenêtre fixe, pas glissante : un compartiment qui reçoit du trafic finit toujours par expirer.
  • La clé stockée est une empreinte salée : aucune adresse IP ni adresse e-mail en clair ne se trouve dans les options ou le cache objet.
  • Normalisation d'IP : IPv4 conservée, IPv6 tronquée au /64 — le client contrôle chaque bit de l'identifiant d'interface, donc une clé complète permettrait de créer un budget neuf à chaque requête.
  • Identité partagée : derrière un CDN ou un proxy non déclaré via OWC_TRUSTED_PROXY, tous les visiteurs arrivent avec la même adresse. Le budget est alors fractionné par visiteur, avec un compteur élargi sur l'adresse partagée. C'est une limite plus grossière : déclarez votre proxy.
  • Filtre owc_throttle_max( $max, $bucket, $window ) — renvoyer 0 désactive la limite.
  • Le limiteur échoue ouvert si le cache objet est indisponible : mieux vaut ne pas limiter que d'empêcher quelqu'un d'exercer ses droits.

La route /consent en détail

  • headers_sent() est lu en tout premier. Si la sortie a déjà commencé, le cookie serveur ne peut pas être posé : la ligne est quand même écrite et la réponse reste 200, avec cookie_set: false et un descripteur cookie (name, value, ttl, path, samesite, secure) que le client pose lui-même.
  • Les catégories que le site n'offre pas sont renvoyées dans dropped_categories, pas ignorées en silence.
  • page_url est validée contre les hôtes du site, avec repli sur un Referer validé de la même façon, sinon la chaîne vide. La clé est toujours présente, pour que le registre n'aille pas rechercher le Referer brut.
  • Si le registre est activé et que l'écriture est refusée, le cookie est annulé et la réponse est un 503 owc_ledger_write_refused. Rien n'est stocké, aucun traceur n'est libéré.
  • cookie_unreadable est de la télémétrie pure : aucun contrôle de validation ne lui est appliqué, précisément pour qu'un champ de diagnostic ne puisse jamais refuser une écriture de consentement. Son seul effet est une note dans le journal des refus.

Réponse type : {ok, state, dropped_categories, logged, cookie_set[, cookie]}. Le champ logged vaut null quand le registre est désactivé.

Codes d'erreur

CodeHTTPSignification
owc_bad_param400Paramètre invalide
owc_bad_categories400Objet categories malformé, trop grand ou non scalaire
owc_bad_email400Adresse e-mail invalide
owc_bad_dsar400Soumission DSAR malformée (piège à robots inclus)
owc_dsar_not_attested400Attestation de l'art. 12.6 absente
owc_no_settings400Aucune clé de réglage connue dans le corps
owc_missing_origin403Ni origine ni jeton : échec fermé
owc_bad_origin403L'origine déclarée n'est pas un hôte de ce site — la réponse liste jusqu'à dix hôtes acceptés
owc_bad_nonce403Nonce invalide sur /scanner/ingest
owc_jsonp_forbidden403Forme JSONP refusée sur /nonce
owc_forbidden401/403Capacité insuffisante
owc_dsar_disabled404Le portail de droits est désactivé côté serveur
owc_no_template404Aucun gabarit pour ce type et ce profil
owc_rate_limited429Limite de débit atteinte
owc_consent_failed500Échec d'application du consentement
owc_dsar_store_failed500Échec d'écriture de la demande
owc_gen_failedvariableGénération de document refusée (champs manquants, barrière de langue)
owc_ledger_write_refused503Le registre a refusé d'écrire — rien n'est enregistré, rien n'est débloqué

Le journal des refus

Les vingt derniers refus d'écriture sont conservés et affichés sur le tableau de bord — uniquement si le journal n'est pas vide. Deux natures y sont distinguées :

  • une entrée avec un statut HTTP est un vrai refus : rien n'a été stocké et le visiteur a vu une erreur ;
  • une entrée avec un statut 0 est un avertissement : le choix a bien été enregistré.

Le panneau affiche côte à côte l'hôte déclaré par l'origine et l'en-tête Host de la requête. C'est exactement ce qui diffère quand vos visiteurs naviguent sur un hôte pendant que WordPress est configuré avec un autre — la cause la plus fréquente d'un consentement qui « ne s'enregistre pas ».

Limites de l'API

  1. /consent et /dsar acceptent des écritures anonymes par conception ; la protection est l'origine plus la limite de débit, pas un nonce.
  2. Le limiteur de débit échoue ouvert sans cache objet.
  3. Le cookie de consentement est host-only : un cookie posé pour l'apex est illisible depuis une page www., et aucun en-tête CORS ne peut corriger cela. Le remède est une URL d'écriture de même origine.
  4. /scanner/ingest n'accepte que manage_options : la sonde ne peut pas venir d'un visiteur.
  5. /settings n'a pas de nonce propre : c'est manage_options plus le contrôle de nonce par cookie du cœur.

Référence des réglages

Tous les réglages tiennent dans une seule option, owc_settings, chargée automatiquement puisqu'elle est lue à chaque page front.

Lecture et écriture en PHP

$reglages = OWC_Core::settings();               // réglages effectifs (défauts + enregistrés + filtres)
$profil   = OWC_Core::setting( 'compliance_profile' );
$brut     = OWC_Core::stored_setting( 'compliance_profile' ); // ignore la réécriture par visiteur

OWC_Core::update_settings( array(
    'consent_renewal_months' => 6,
    'compliance_strict'      => true,
) );

update_settings() ne fusionne que les clés soumises par-dessus l'existant, jamais l'ensemble des défauts. C'est ce qui permet à la ligne d'option de rester vide tant que vous n'avez rien personnalisé, et donc aux textes de suivre la langue du site.

Le résultat de settings() est mémoïsé, avec une clé fondée sur l'ensemble des rappels du filtre owc_settings : un module qui enregistre son filtre après la première lecture invalide le mémo au lieu d'être ignoré. Le mémo est vidé à chaque écriture de l'option.

Valeurs par défaut

[
    // Bannière
    'banner_enabled'            => true,
    'banner_position'           => 'bottom-bar',   // bottom-bar | bottom-card | center-modal | top-bar
    'banner_style'              => 'auto',         // auto | light | dark
    'banner_accept_all'         => true,
    'banner_reject_all'         => true,
    'banner_preferences'        => true,
    'banner_close_x'            => false,          // croix = refus implicite
    'banner_show_logo'          => true,
    'text_title'                => 'We use cookies',
    'text_message'              => 'We use cookies to personalise content, measure audience, '
                                 . 'and provide social media features. You can accept or reject, '
                                 . 'and change your choice at any time.',
    'text_accept_all'           => 'Accept all',
    'text_reject_all'           => 'Reject all',
    'text_preferences'          => 'Customize',
    'text_save'                 => 'Save my choices',

    // Bouton flottant
    'floating_button_enabled'   => true,
    'floating_button_position'  => 'bottom-left',
    'floating_button_style'     => 'pill',
    'floating_button_label'     => 'Cookies',

    // Consentement
    'consent_renewal_months'    => 12,             // 0 est converti en 13, pas en « jamais »
    'consent_policy_hash_check' => true,

    // Conformité
    'compliance_profile'        => 'gdpr',
    'compliance_strict'         => true,
    'geo_enabled'               => false,
    'geo_default_profile'       => 'gdpr',
    'geo_mmdb_enabled'          => false,

    // IAB TCF v2.2
    'tcf_enabled'               => false,
    'tcf_cmp_id'                => 0,
    'tcf_publisher_country'     => 'FR',
    'tcf_publisher_purposes_li' => [],
    'tcf_special_features'      => [],

    // CCPA / GPC
    'ccpa_inject_footer'        => true,
    'gpc_honor'                 => true,

    // Google Consent Mode v2
    'gcm_enabled'               => true,
    'gcm_ads_data_redaction'    => true,
    'gcm_url_passthrough'       => true,

    // Catégories
    'cat_necessary_available'   => true,
    'cat_functional_available'  => true,
    'cat_analytics_available'   => true,
    'cat_marketing_available'   => true,
    'cat_preferences_available' => true,
    'cat_social_available'      => true,
    'cat_necessary_label'       => 'Necessary',
    'cat_necessary_desc'        => 'Strictly required for the site to function …',
    'cat_functional_label'      => 'Functional',
    'cat_functional_desc'       => 'Enhance the experience (chat, embedded videos, maps) …',
    'cat_analytics_label'       => 'Statistics',
    'cat_analytics_desc'        => 'Help us understand how you use the site (anonymously) …',
    'cat_marketing_label'       => 'Marketing',
    'cat_marketing_desc'        => 'Enable us to show you ads and content tailored …',
    'cat_preferences_label'     => 'Preferences',
    'cat_preferences_desc'      => 'Remember your interface choices (layout, saved filters).',
    'cat_social_label'          => 'Social & embeds',
    'cat_social_desc'           => 'Allow embedded social content (YouTube, Instagram, X) to load.',

    // Registre
    'ledger_enabled'            => true,
    'ledger_retention_days'     => 1825,           // 5 ans ; 0 = illimité
    'ledger_hash_algo'          => 'sha256',

    // Scanner
    'scanner_enabled'           => false,
    'scanner_frequency'         => 'weekly',
    'scanner_alert_email'       => '',
    'scanner_max_urls'          => 25,
    'scanner_timeout'           => 8,
    'scanner_probe_mode'        => 'admins',

    // Portail de droits
    'dsar_enabled'              => true,
    'dsar_email'                => '',
    'dsar_response_days'        => 30,
    'dsar_notify_email'         => '',
    'dsar_token_ttl_days'       => 7,

    // Blocage
    'blocker_unknown_script_policy' => 'allow',
    'blocker_unknown_iframe_policy' => 'block',
    'blocker_allowlist'             => '',         // un hôte par ligne
    'blocker_block_resource_hints'  => true,

    // Intégration et cycle de vie
    'forms_integration_enabled' => true,
    'auto_footer_menu_inject'   => false,
    'delete_data_on_uninstall'  => false,

    // Identité légale
    'legal_company_name'         => '',
    'legal_company_legal_form'   => '',
    'legal_company_capital'      => '',
    'legal_company_address'      => '',
    'legal_company_email'        => '',
    'legal_company_phone'        => '',
    'legal_country'              => 'FR',
    'legal_company_reg_number'   => '',
    'legal_company_reg_place'    => '',
    'legal_company_vat'          => '',
    'legal_publication_director' => '',
    'legal_eu_representative'    => '',
    'legal_host_name'            => '',
    'legal_host_address'         => '',
    'legal_host_phone'           => '',
    'legal_host_url'             => '',
    'legal_profession_body'      => '',
    'legal_profession_title'     => '',
    'legal_profession_state'     => '',
    'legal_profession_rules'     => '',
    'legal_mediator_name'        => '',
    'legal_mediator_address'     => '',
    'legal_mediator_url'         => '',
    'legal_dpo_name'             => '',
    'legal_dpo_email'            => '',
    'legal_dpa_authority'        => '',            // vide à dessein : dérivée du pays + profil
    'legal_transfer_countries'   => '',
    'legal_third_parties_note'   => '',
    'legal_automated_decisions'  => false,
    'legal_automated_decisions_desc' => '',
    'legal_minor_age'            => 15,
    'legal_data_provision_note'  => '',
    'legal_currency'             => 'EUR',
    'legal_tax_label'            => 'TTC',
    'legal_business_type'        => 'auto',
    'retention_form_data_days'   => 1095,
    'retention_dsar_days'        => 1095,
    'policy_disclaimer'          => true,

    // Liens
    'link_privacy_policy'        => '',
    'link_cookie_policy'         => '',
    'link_terms'                 => '',
    'link_legal_notice'          => '',
]

Listes fermées

Une valeur hors liste est ramenée au défaut.

RéglageValeurs acceptées
banner_positionbottom-bar, bottom-card, center-modal, top-bar
banner_styleauto, light, dark
floating_button_positionbottom-left, bottom-right, top-left, top-right
floating_button_styleicon, pill
compliance_profile, geo_default_profilegdpr, ccpa, lgpd, popia, pipl, dpdp, quebec, uk_pecr, ch_nfadp, au, us_generic
ledger_hash_algosha256, sha3-256 (intersectés avec ce que PHP supporte)
scanner_frequencyhourly, twicedaily, daily, weekly
scanner_probe_modeadmins, off
legal_business_typeauto, vitrine, rental, ecommerce, services, saas, content
blocker_unknown_script_policy, blocker_unknown_iframe_policyallow, block

Bornes des entiers

RéglageBornes
consent_renewal_months0 à 13 (0 est traité comme 13)
dsar_response_days1 à 30
dsar_token_ttl_days1 à 90
ledger_retention_days0 à 3650 (0 = illimité)
scanner_max_urls1 à 500
scanner_timeout1 à 60
legal_minor_age13 à 18
retention_form_data_days1 à 3650
retention_dsar_days1 à 3650
tcf_cmp_id0 à 4095

Assainissement

  • text_message est le seul champ à HTML riche ; il accepte le HTML autorisé dans un article WordPress, et la bannière ne rend au final que <a href target rel>.
  • Champs multi-lignes : legal_company_address, legal_eu_representative, legal_host_address, legal_mediator_address, legal_third_parties_note, legal_automated_decisions_desc, legal_data_provision_note, blocker_allowlist, et toute clé se terminant par _desc.
  • URLs : toute clé préfixée link_ ou suffixée _url, plus legal_profession_rules.
  • Adresses e-mail : toute clé contenant email.
  • Tout le reste : texte simple.
  • Une valeur non scalaire soumise sur un réglage scalaire est ignorée, la clé n'est pas écrite.

Où se règle quoi

OngletRéglages
Bannerbanner_*, text_*, floating_button_*, consent_renewal_months, consent_policy_hash_check
Compliancecompliance_profile, compliance_strict, gcm_*, blocker_*, ccpa_inject_footer, gpc_honor, tcf_*, geo_*, auto_footer_menu_inject, forms_integration_enabled, delete_data_on_uninstall, link_*
Legal identitylegal_*, retention_form_data_days, retention_dsar_days, policy_disclaimer
Categoriescat_*_available, cat_*_label, cat_*_desc
Audit ledgerledger_enabled, ledger_retention_days (l'algorithme est affiché, pas modifiable)
Scannerscanner_*
DSAR requestsdsar_*

Tables

TableContenu
{prefix}owc_ledgerRegistre de consentement chaîné
{prefix}owc_dsarDemandes d'exercice de droits
{prefix}owc_scannerTrouvailles du scanner
{prefix}owc_scriptsCatalogue de signatures du blocage
{prefix}owc_form_linksLiaisons consentement ↔ soumission OW Forms

Un assistant PHP owc_table( 'ledger' | 'dsar' | 'scanner' | 'scripts' ) recalcule le nom à partir du préfixe courant : utilisez-le dans un switch_to_blog(), les constantes étant figées pour la durée de la requête.

Autres options et transients

Options : owc_settings, owc_version, owc_pending_upgrade, owc_upgrade_lock, owc_ledger_head, owc_ledger_trim, owc_ledger_key_source, owc_scanner_secret, owc_scanner_lock, owc_scanner_queue, owc_scanner_status, owc_scanner_alerted, owc_scanner_probe_status, owc_scanner_manual, owc_refusal_log, owc_tcf_status, owc_tcf_gvl_version, owc_forms_db_version, plus les drapeaux de migration (owc_catalog_sanitized_v2, owc_match_target_migrated_v1, owc_ledger_hmac_migrated_v1, owc_inline_sig_labels_cleaned_v1, owc_scanner_secret_rotated_v1).

Transients : owc_catalog_<version>, owc_invalid_patterns, owc_gvl_cache, owc_gvl_stub, owc_gvl_etag, owc_gvl_retry, owc_mmdb_<empreinte>, owc_policies_hash, owc_site_analysis, owc_scanner_last_run, owc_scanner_run_result, owc_scanner_pruned, owc_scanner_fail_alerted, owc_ledger_verify_result, owc_ledger_write_error, owc_policy_gen_result, owc_dsar_sla_alert, owc_dsar_admin_result, et les compteurs de limitation de débit.

Constantes reconnues dans wp-config.php

ConstanteEffet
OWC_LEDGER_KEYClé HMAC du registre, et clé dérivée du cookie de détection régionale
OWC_TRUSTED_PROXY / OWC_TRUSTED_PROXIESPlages CIDR des proxys de confiance ; sans elles, les en-têtes d'IP client sont ignorés
OWC_GEO_TRUSTED_HEADERScloudflare, cloudfront, proxy ou all
OWC_BEHIND_CLOUDFLARERaccourci pour l'en-tête de pays Cloudflare
OWC_BEHIND_CLOUDFRONTRaccourci pour l'en-tête de pays CloudFront

Les crochets développeur

Filtres

FiltreRôle
owc_settingsRéglages effectifs. La réponse doit dépendre de quelque chose de stable pour toute la requête
owc_banner_textsTextes de la bannière
owc_bootstrap_configConfiguration statique imprimée dans <head>jamais de donnée par visiteur
owc_scripts_catalogLignes du catalogue avant validation
owc_blocker_allowlistHôtes jamais bloqués
owc_scanner_urlsURLs à balayer (re-restreintes à l'hôte du site ensuite)
owc_throttle_maxBudget d'une limite de débit ; 0 la désactive
owc_allowed_request_hostsHôtes acceptés pour une écriture publique
owc_allow_headerless_writeLève l'échec fermé sur les écritures sans origine (défaut false)
owc_geo_mmdb_pathChemin de la base MaxMind
owc_tcf_purpose_mapCorrespondance catégories → finalités TCF
owc_tcf_stub_configConfiguration statique du stub TCF
owc_policy_templateCorps brut d'un gabarit — désactive le verrou de langue
owc_policy_varsVariables d'un document
owc_policy_htmlHTML final d'un document
owc_dsar_typesTypes de demandes proposés et acceptés
owc_dsar_form_noticeMention d'information sous le formulaire
owc_dsar_export_bundleContenu du bundle de portabilité
owc_dsar_show_fulfilment_panelAffichage du panneau d'exécution

Actions

ActionSignature
owc_consent_updated( array $categories, string $event, string $source )
owc_consent_cookie_not_sent( array $cookie, string $event )
owc_ledger_lock_failed( string $event, string $token )
owc_ledger_write_failed( $error, string $event, string $token )
owc_ledger_anchor( array $anchor )
owc_ledger_pruned( int $deleted, int $days )
owc_catalog_updated
owc_gpc_honored( array $categories )
owc_tcf_inactive( string $reason )
owc_dsar_submitted( string $type, string $email, string $message )
owc_dsar_verified( int $id, array $row )
owc_dsar_fulfilled( int $id, string $what, $trace )
owc_dsar_mail_failed( int $id, string $email, string $kind )

Crons : owc_daily_maintenance, owc_ledger_retention, owc_scanner_run, owc_scanner_run_batch, owc_tcf_refresh_gvl, owc_run_upgrade.

Internationalisation

Le domaine de traduction est ow-consent, le chemin /languages. La langue source est l'anglais. Le paquet livre le gabarit ow-consent.pot et une traduction française complète.

Rappel du mécanisme décrit plus haut : tant qu'un libellé ou un texte de bannière n'est pas personnalisé dans les réglages, il suit la langue du site. Dès que vous saisissez votre propre valeur, elle est servie telle quelle, quelle que soit la langue du visiteur.

Les corps des documents légaux ne passent pas par ce mécanisme : leur langue suit la juridiction, jamais la locale de l'administrateur.


Dépannage

La bannière ne s'affiche pas

Parcourez la liste dans l'ordre :

  1. banner_enabled est-il activé ? Le badge en haut de l'écran d'administration le dit.
  2. Votre thème appelle-t-il wp_head() et wp_footer() ? Sans eux, la bannière refuse de s'imprimer plutôt que de produire un balisage inerte.
  3. Le visiteur a-t-il déjà fait un choix ? La bannière ne s'affiche que sur <html data-owc="none">. Testez en navigation privée ou avec window.OWCBanner.reset().
  4. Le visiteur a-t-il JavaScript ? Sans lui, la bannière reste masquée — délibérément — et le bloc <noscript> prend le relais.
  5. Un cache de page sert-il une version antérieure à l'activation ? Videz-le.

La bannière revient à chaque page alors que le choix a été enregistré

Trois causes, par ordre de fréquence.

  1. L'hôte parcouru diffère de l'hôte configuré dans WordPress (www. contre apex, alias, domaine de préproduction). Le navigateur refuse alors de stocker le cookie pendant que WordPress répond 200. Ouvrez le tableau de bord : le journal des refus affiche côte à côte l'origine et l'en-tête Host. Le remède est de servir le site sur un seul hôte canonique.
  2. consent_policy_hash_check est activé et vos documents ont changé — c'est le comportement voulu, le visiteur est re-sollicité après une modification de politique.
  3. Vous êtes sur une version antérieure à 1.4.3. Le cookie était alors encodé deux fois et le navigateur n'arrivait pas à le relire, alors que le serveur, lui, le lisait très bien. Mettez à jour : les cookies écrits avant la correction restent lisibles.

« Votre choix n'a pas pu être enregistré » — erreur refused ou 403

Regardez data-owc-code sur l'élément de message, ou le journal des refus du tableau de bord.

  • owc_bad_origin : l'origine déclarée n'est pas un hôte reconnu. La réponse liste les hôtes acceptés. Si votre site est légitimement joint par plusieurs noms, ajoutez-les avec le filtre owc_allowed_request_hosts.
  • owc_missing_origin : ni origine, ni jeton. C'est typiquement une extension de confidentialité qui supprime les en-têtes, ou un proxy qui les réécrit.
  • owc_bad_nonce : ne concerne que la sonde du scanner, pas l'écriture de consentement — le nonce n'est jamais un veto sur /consent.

Erreur ratelimit — 429

Les limites sont volontairement basses sur les écritures publiques. Deux causes :

  1. Un CDN ou un proxy non déclaré : tous vos visiteurs arrivent avec la même adresse et partagent le même budget. Déclarez-le avec OWC_TRUSTED_PROXY dans wp-config.php.
  2. Un test automatisé qui envoie plus de 30 écritures en 5 minutes depuis la même adresse.

Un 503 owc_ledger_write_refused

Le registre a refusé d'écrire, donc rien n'a été enregistré et aucun traceur n'a été libéré. C'est le comportement voulu : un consentement qu'on ne peut pas prouver ne doit pas être revendiqué.

  1. Vérifiez que la table {prefix}owc_ledger existe. L'onglet Audit ledger affiche un message lisible « pas encore installé » plutôt qu'un écran blanc.
  2. Vérifiez les droits d'écriture de l'utilisateur MySQL.
  3. En dernier recours, désactivez temporairement ledger_enabled pour rétablir le service — en sachant que vous perdez la preuve pendant ce temps.

Des traceurs se chargent encore avant le consentement

  1. compliance_strict est-il activé ? Sans lui, seuls les scripts mis en file par WordPress sont traités.
  2. Le traceur est-il au catalogue ? Un script tiers inconnu est autorisé par défaut. Lancez un balayage, classez la trouvaille : cela écrit la règle de blocage.
  3. Est-il injecté par du JavaScript first-party ? La garde dynamique ne couvre que vingt hôtes. Ajoutez une règle de catalogue sur le domaine du traceur.
  4. Est-ce un cookie posé par en-tête Set-Cookie ? Aucun blocage client ne peut le retenir. Il faut le traiter à la source, côté serveur.
  5. Passez blocker_unknown_script_policy sur block — puis testez soigneusement le site, parce que ce réglage bloque tout tiers non reconnu.

Une intégration légitime est bloquée

Ajoutez son hôte dans blocker_allowlist, un par ligne, ou par le filtre owc_blocker_allowlist. Vérifiez aussi blocker_unknown_iframe_policy : les iframes inconnues sont bloquées par défaut, c'est le cas le plus fréquent.

Si l'intégration doit rester conditionnée au consentement, classez-la plutôt dans la bonne catégorie : le substitut visuel porte un bouton « Accepter … » qui la débloque en un clic.

Le scanner ne trouve rien

  1. Est-il activé ? Il est désactivé par défaut.
  2. WP-Cron fonctionne-t-il ? Avec DISABLE_WP_CRON et sans cron système, le balayage planifié ne part jamais. Lancez-en un à la main pour vérifier.
  3. Le loopback HTTP est-il possible ? Une authentification HTTP de préproduction ou un pare-feu bloque le balayage. Le panneau de couverture affiche la première erreur.
  4. Aucun JavaScript n'est exécuté par le balayage serveur. Consultez une page du site en tant qu'administrateur connecté pour que la sonde rapporte ce qu'un gestionnaire de balises injecte.

Dans tous les cas, lisez le panneau « ce que ce balayage couvre réellement » avant de conclure : il distingue « rien trouvé » de « rien lu ».

La génération d'un document est refusée

Deux causes possibles, et le message le dit :

  • Des champs obligatoires sont vides — la liste des clés manquantes est affichée. Complétez l'onglet Legal identity.
  • Barrière de langue — vous demandez des mentions légales ou des CGU/CGV pour une juridiction non francophone. Le bouton est masqué dans ce cas. La seule voie est de fournir votre propre corps via le filtre owc_policy_template.

L'e-mail de vérification DSAR n'arrive pas

C'est presque toujours la délivrabilité, pas le plugin.

  1. La réponse de l'API porte mail_sent: false quand l'envoi a échoué, et le formulaire affiche alors un message dédié renvoyant vers l'adresse de contact.
  2. Installez une extension SMTP. La fonction mail() de PHP est rejetée par la plupart des serveurs de réception.
  3. Vérifiez SPF, DKIM et DMARC de votre domaine. Le plugin ne réécrit jamais l'adresse d'expédition — c'est ce qui casserait SPF — il pose seulement un Reply-To.
  4. Vérifiez que dsar_email est une adresse valide.

Tant que l'e-mail n'est pas reçu et confirmé, la demande reste pending et le délai légal ne démarre pas.

Le module TCF ne s'active pas

Ouvrez l'onglet Compliance : un avis d'administration nomme la cause.

  • missing_cmp_id : tcf_cmp_id vaut 0. Vous devez obtenir un CMP ID auprès d'IAB Europe ; le plugin n'en fournit pas et refuse d'émettre une chaîne avec un identifiant 0.
  • CMP ID supérieur à 4095 : la valeur ne tient pas dans le champ de 12 bits de la chaîne et désignerait un autre CMP.
  • no_gvl : la Global Vendor List n'a pas encore été téléchargée. Elle arrive par un cron quotidien ; vérifiez que WP-Cron tourne et que votre serveur peut joindre vendor-list.consensu.org en HTTPS sortant.

Tous les visiteurs reçoivent le profil configuré, malgré la détection régionale

  1. geo_enabled est-il activé ? Il est désactivé par défaut.
  2. Avez-vous déclaré votre proxy ? Sans OWC_BEHIND_CLOUDFLARE, OWC_BEHIND_CLOUDFRONT ou OWC_GEO_TRUSTED_HEADERS dans wp-config.php, les en-têtes de pays sont ignorés, parce qu'ils sont forgeables.
  3. Le fichier MaxMind est-il en place ? Le plugin ne le télécharge pas et n'offre pas d'écran de téléversement.
  4. C'est peut-être normal : le premier affichage d'un visiteur servi par le cache utilise le profil configuré tant que le cookie de 24 heures n'existe pas. Le tableau de bord affiche un bloc de diagnostic avec le pays détecté, le profil appliqué et la source.

L'administration signale que la clé du registre vit en base

C'est l'avis notice_ledger_key. Il signifie que AUTH_KEY et AUTH_SALT ne sont pas dans wp-config.php — WordPress les stocke alors dans la base — et que OWC_LEDGER_KEY n'est pas définie non plus. La chaîne continue d'être construite, mais quiconque a accès à la base peut la re-signer : la revendication d'infalsifiabilité ne tient plus.

Le remède est d'ajouter OWC_LEDGER_KEY (et, tant que vous y êtes, AUTH_KEY et AUTH_SALT) dans wp-config.php. La vérification distingue ensuite une rotation de clé d'une réécriture, donc ce changement n'invalide pas votre historique.

Le plugin ne fait rien du tout, avec un avis rouge

Votre WordPress est antérieur à 6.2. Le plugin refuse de démarrer, et l'avis dit explicitement que rien n'est bloqué et qu'aucun consentement n'est enregistré. Mettez WordPress à jour, ou désactivez le plugin et retirez la bannière de vos pages en attendant.

Des règles de catalogue « ne peuvent pas se déclencher »

Un avis d'administration les signale, et l'onglet Tracker catalogue a un filtre dédié. Quatre motifs possibles : motif vide ou composé uniquement de caractères invisibles, expression régulière qui ne compile pas ou qui rétro-suit de façon catastrophique, motif trop court et sans point pour cibler une URL, ou catégorie qui n'existe plus sur le site. Corrigez ou supprimez ces lignes : elles donnent une fausse impression de couverture.


FAQ

Le plugin fonctionne-t-il derrière un cache de page ? Oui, et toute son architecture est bâtie autour de cette contrainte. Le HTML produit est identique pour tous les visiteurs ; le consentement est lu dans le navigateur et appliqué avant le premier rendu. Aucun nonce n'est imprimé dans du HTML cachable. Seules les réponses réellement personnelles sont marquées non cachables.

Remplace-t-il un CMP payant ? Sur la plupart des sites, il couvre le même terrain : bannière, blocage, journal d'audit, demandes de droits, routage géographique et documents générés. Deux choses qu'il ne fait pas : ce n'est pas un CMP enregistré auprès d'IAB Europe, et il ne fournit pas de conseil juridique ni de documents relus — le générateur produit des brouillons que vous devez faire valider.

Le registre est-il infalsifiable ? Il est inviolable en évidence, sous une condition. Chaque ligne est signée par HMAC sur l'empreinte de la précédente : une ligne modifiée ou supprimée casse la chaîne et la vérification dit où. La garantie repose sur le fait que la clé de signature vit hors de la base. C'est le cas si AUTH_KEY et AUTH_SALT sont dans wp-config.php, ou si vous définissez OWC_LEDGER_KEY. Sinon, WordPress stocke les sels en base et un attaquant qui y accède pourrait re-signer la chaîne — le plugin détecte cette situation et vous prévient.

Le scanner ralentit-il le site ? Non. Il est désactivé par défaut, et une fois activé il tourne sur WP-Cron à la fréquence que vous choisissez, en récupérant un échantillon de vos propres pages côté serveur. Il ne s'exécute jamais pendant la consultation d'un visiteur.

Le scanner trouve-t-il les cookies ? Partiellement, et c'est important à comprendre. Le balayage serveur lit les en-têtes Set-Cookie de vos pages : il trouve donc les cookies posés par le serveur, y compris les HttpOnly. Il n'exécute aucun JavaScript, donc les cookies écrits par des scripts dans le navigateur ne lui sont pas visibles ; ceux-là sont collectés par une sonde qui ne tourne que pour un administrateur connecté. Traitez le résultat comme un inventaire de ce qui a été vu, pas comme une liste exhaustive — les deux écrans le disent au lieu de vous laisser supposer le contraire.

Comment une demande de droits est-elle exécutée ? La personne soumet le formulaire et reçoit un e-mail de vérification. Confirmer la demande sur la page liée démarre le délai de l'article 12.3 et ouvre une demande WordPress native. Depuis l'écran DSAR, vous téléchargez un export JSON produit par tous les exportateurs enregistrés sur le site, vous lancez tous les effaceurs, et vous clôturez avec une réponse écrite qui reste attachée à la demande. Ce que couvre l'export dépend donc des extensions installées ; celles qui n'enregistrent aucun de ces crochets doivent être traitées à la main.

Puis-je utiliser le module TCF pour AdSense ou Ad Manager ? Seulement avec votre propre CMP ID délivré par IAB Europe, et même dans ce cas avec une réserve. Sans CMP ID, le module ne se charge pas du tout. Avec un CMP ID, il émet une chaîne TC correctement encodée et le panneau affiche chaque finalité et chaque fonctionnalité spéciale que cette chaîne peut revendiquer — mais la politique TCF exige d'un CMP enregistré des choix au niveau finalité et au niveau vendeur. Ici les interrupteurs de finalités suivent les catégories et il n'y a aucun choix vendeur : ce n'est pas un CMP enregistré, et les vendeurs sont fondés à rejeter son signal. Si un revenu publicitaire sous TCF compte pour votre site, utilisez un CMP certifié.

Dans quelle langue les documents sont-ils générés ? La politique de confidentialité et la politique cookies existent pour les onze profils, dans la langue de la juridiction : anglais, français pour la France, la Belgique, le Luxembourg et le Québec, portugais pour le Brésil. Les mentions légales et les CGU/CGV n'existent qu'en français ; pour toute autre juridiction, le générateur refuse de les produire plutôt que de publier un document inadapté.

Que puis-je changer sur la bannière ? Quatre positions, un thème clair, sombre ou automatique, un logo optionnel, les libellés, et un bouton flottant de réouverture. Tout élément portant la classe owc-open-preferences rouvre le panneau. « Tout refuser » est rendu avec la même prominence que « Tout accepter ». La croix de fermeture est désactivée par défaut ; activée, elle enregistre un refus complet, jamais une fermeture silencieuse. Le panneau est utilisable au clavier, ses interrupteurs portent un état visible, et la modale piège puis restitue le focus. Les textes sont traduisibles et remplaçables par le filtre owc_banner_texts.

Que bloque exactement le blocage automatique ? Les scripts tiers, les snippets de suivi en ligne, les iframes, les pixels de mesure, les feuilles de style analytics et marketing, et les sources de médias tiers sont réécrits avant consentement et libérés ensuite, dans l'ordre du document. Les indices de ressources vers un tiers catalogué sont supprimés plutôt que différés, puisqu'un indice ouvre une connexion et n'a rien à restaurer. Les feuilles et polices servies par un hôte classé fonctionnel sont laissées intactes. Les attributs de chargement paresseux des extensions de cache sont neutralisés pour qu'un chargeur ne restaure pas une URL bloquée. Le plugin livre 175 signatures, éditables depuis l'écran Tracker catalogue et extensibles par filtre.

Qu'est-ce qui est inscrit au registre ? Chaque action de consentement — acceptation, refus, enregistrement partiel, opt-out GPC, renouvellement, retrait — est ajoutée à la table chaînée. Une ligne contient une IP pseudonymisée, une empreinte de user-agent, l'URL de page, le profil de conformité, une empreinte des documents en vigueur, une empreinte de la bannière réellement affichée et la version du plugin — les éléments qui permettent de reconstituer ce que le visiteur a vu.

Quelles protections y a-t-il sur le formulaire de droits ? Un piège à robots, un délai minimum de remplissage, une attestation obligatoire vérifiée côté serveur, trois limites de débit distinctes, un jeton à usage unique stocké uniquement sous forme d'empreinte, et une confirmation qui exige une action explicite sur la page — de sorte qu'un scanner de liens de messagerie ne puisse pas confirmer une identité à la place de la personne. Une demande dont l'auteur n'a jamais confirmé son identité ne peut être ni exportée, ni effacée, ni enregistrée comme honorée, et ce contrôle est appliqué côté serveur, pas seulement en masquant des boutons.

Quels signaux Google Consent Mode v2 sont émis ? Les sept : ad_storage, ad_user_data, ad_personalization, analytics_storage, functionality_storage, personalization_storage et security_storage. Les six catégories y sont associées depuis une source de vérité unique, si bien que la bannière, le blocage et le bootstrap ne peuvent pas diverger.

Comment sont gérés l'opt-out CCPA et GPC ? Un contrôle « Do Not Sell or Share » est injecté en pied de page ou placé par shortcode. Le premier clic enregistre l'opt-out, comme l'exigent les règlements californiens, plutôt que d'ouvrir un panneau. Le signal Global Privacy Control est traité comme un opt-out contraignant sous les profils américains, une fois par session de navigation, et le visiteur est prévenu s'il a remplacé un choix qu'il avait enregistré. Sous les profils de la famille RGPD, GPC est traité comme un indice : les catégories optionnelles sont pré-refusées dans l'interface, la bannière reste affichée, et rien n'est enregistré — parce que le consentement y doit être un acte positif.

Quels cookies le plugin dépose-t-il lui-même ? Tous sont des cookies internes, aucun ne sert au suivi, et ils sont déclarés dans la politique cookies générée : owc_consent (les choix, le jeton pseudonyme du navigateur et le profil ; durée suivant le réglage de renouvellement, 12 mois par défaut, plafonnée à 13), owc_geo (pays et profil détectés, 24 heures, sans aucune adresse IP), owc_gpc (marqueur de session), owc_gpc_notice (5 minutes), et euconsent-v2 (chaîne TCF, uniquement si le module est activé et après une action du visiteur).

Fonctionne-t-il en multisite ? Oui. Chaque site du réseau a ses propres tables et ses propres réglages. Une activation réseau parcourt tous les sites uniquement si le réseau en compte au plus 200 ; au-delà, chaque site est provisionné à sa première requête.

Et si je supprime le plugin ? Le registre, les trouvailles du scanner, le catalogue, les liaisons OW Forms, les options et les réglages survivent par défaut. Deux choses partent toujours, quel que soit votre réglage : les six tâches planifiées, et la table des demandes de droits — c'est la seule qui contient des données personnelles directement identifiantes concernant des tiers, et une fois le plugin parti, plus rien n'en borne la rétention et aucun écran ne permet d'y répondre. Exportez vos demandes avant de supprimer le plugin. Pour un nettoyage complet du reste, activez « Delete all data on uninstall » avant la suppression.

Où trouver le support ?