Vingt-quatre mois : c’est la durée maximale recommandée pour conserver un CV non retenu, sans nouveau consentement du candidat. Passé ce délai, l’extension de collecte utilisée par nombre de cabinets de recrutement indépendants continue pourtant de le stocker indéfiniment, faute d’avoir prévu cette limite dès la conception du schéma de données. Ce problème se résout au moment de la conception de l’extension de collecte, pas en ajoutant une case à cocher supplémentaire dans un formulaire déjà en production.
Ce guide construit une extension qui traite les candidatures d’un cabinet de recrutement indépendant, avec une durée de conservation appliquée automatiquement plutôt que promise dans une politique de confidentialité jamais vérifiée par le code. Il ne traite pas la publication d’offres d’emploi — uniquement le traitement des candidatures reçues.
Problème : le CV vit plus longtemps que sa raison d’être
Un formulaire de candidature classique enregistre un fichier PDF dans la médiathèque, crée une ligne dans une table de contacts, et s’arrête là. Rien ne relie cette donnée à une date d’expiration. Deux ans plus tard, l’archive contient des centaines de CV de candidats non retenus, sans qu’aucun mécanisme n’ait déclenché leur suppression — ce qui constitue une conservation excessive au sens du RGPD, même en l’absence de toute mauvaise intention.
Snippet commenté : structurer la candidature avec sa date d’expiration
La solution consiste à inscrire la durée de conservation dans le schéma de données dès la création de la candidature, sous la forme d’une métadonnée de date d’expiration calculée automatiquement.
function enregistrer_candidature( array $donnees, int $fichier_id ) : int {
$candidature_id = wp_insert_post( [
'post_type' => 'candidature',
'post_status' => 'private',
'post_title' => sanitize_text_field( $donnees['nom'] ),
] );
update_post_meta( $candidature_id, '_cv_fichier_id', $fichier_id );
update_post_meta( $candidature_id, '_consentement_date', current_time( 'mysql' ) );
update_post_meta(
$candidature_id,
'_date_expiration',
gmdate( 'Y-m-d H:i:s', strtotime( '+24 months' ) )
);
return $candidature_id;
}

Snippet commenté : la purge automatique via une tâche planifiée
Une tâche planifiée quotidienne parcourt les candidatures dont la date d’expiration est dépassée, supprime le fichier PDF associé, puis le post lui-même. La suppression du fichier ne doit pas être oubliée : garder le post sans le CV, ou l’inverse, laisse une trace partielle tout aussi problématique.
function purger_candidatures_expirees() {
$candidatures = get_posts( [
'post_type' => 'candidature',
'posts_per_page' => 50,
'meta_query' => [ [
'key' => '_date_expiration',
'value' => current_time( 'mysql' ),
'compare' => '<=',
'type' => 'DATETIME',
] ],
] );
foreach ( $candidatures as $candidature ) {
$fichier_id = get_post_meta( $candidature->ID, '_cv_fichier_id', true );
if ( $fichier_id ) {
wp_delete_attachment( $fichier_id, true );
}
wp_delete_post( $candidature->ID, true );
}
}
if ( ! wp_next_scheduled( 'purge_candidatures_quotidienne' ) ) {
wp_schedule_event( time(), 'daily', 'purge_candidatures_quotidienne' );
}
add_action( 'purge_candidatures_quotidienne', 'purger_candidatures_expirees' );
Variante : prolonger la conservation pour un vivier assumé
Certains cabinets souhaitent constituer un vivier de profils intéressants au-delà de la durée par défaut. Cette prolongation ne peut pas être décidée unilatéralement : elle exige un nouveau consentement explicite du candidat, distinct de celui donné pour la candidature initiale. La variante ajoute donc un champ de renouvellement, avec sa propre date d’horodatage.
function prolonger_conservation( int $candidature_id ) {
update_post_meta( $candidature_id, '_consentement_vivier_date', current_time( 'mysql' ) );
update_post_meta(
$candidature_id,
'_date_expiration',
gmdate( 'Y-m-d H:i:s', strtotime( '+24 months' ) )
);
}
Sans cet appel explicite, déclenché uniquement après une action volontaire du candidat — par exemple un lien cliqué dans un e-mail de relance —, la candidature suit son cycle de vie par défaut et disparaît à l’échéance initiale.
Variante : exporter la candidature à la demande du candidat
Le droit à la portabilité impose de pouvoir remettre au candidat une copie structurée de ses données sur simple demande. Une route REST protégée, accessible uniquement par l’administrateur du cabinet, génère un export JSON de la candidature et de son historique de consentement :
add_action( 'rest_api_init', function () {
register_rest_route( 'cabinet/v1', '/candidatures/(?P<id>\d+)/export', array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'exporter_candidature',
'permission_callback' => function () {
return current_user_can( 'manage_options' );
},
'args' => array(
'id' => array(
'validate_callback' => function ( $valeur ) {
return is_numeric( $valeur ) && (int) $valeur > 0;
},
),
),
) );
} );
function exporter_candidature( WP_REST_Request $request ) {
$candidature = get_post( (int) $request['id'] );
if ( ! $candidature || 'candidature' !== $candidature->post_type ) {
return new WP_Error( 'candidature_introuvable', 'Candidature inconnue.', array( 'status' => 404 ) );
}
return rest_ensure_response( array(
'nom' => $candidature->post_title,
'recue_le' => get_post_meta( $candidature->ID, '_consentement_date', true ),
'expire_le' => get_post_meta( $candidature->ID, '_date_expiration', true ),
'consentement_vivier' => get_post_meta( $candidature->ID, '_consentement_vivier_date', true ) ?: null,
'cv' => wp_get_attachment_url( (int) get_post_meta( $candidature->ID, '_cv_fichier_id', true ) ),
) );
}
La route est réservée aux administrateurs grâce à son permission_callback, et elle ne renvoie que ce que le candidat a fourni ou que le cabinet a enregistré à son sujet : nom, dates, consentements, lien vers le fichier. Elle ne contient aucune donnée d’un autre candidat. Les notes internes d’un recruteur sont un cas délicat : elles peuvent relever du droit d’accès, et la frontière mérite d’être tranchée avec votre conseil juridique avant la mise en production.
Brancher l’export sur les outils du cœur
Depuis WordPress 4.9.6, l’écran « Exporter les données personnelles » du menu Outils sait interroger les extensions. Plutôt que de multiplier les routes sur mesure, une extension peut déclarer un exporteur que le cœur appellera lui-même, par adresse électronique :
add_filter( 'wp_privacy_personal_data_exporters', function ( $exporteurs ) {
$exporteurs['cabinet-candidatures'] = array(
'exporter_friendly_name' => __( 'Candidatures', 'cabinet' ),
'callback' => 'exporter_donnees_candidat',
);
return $exporteurs;
} );
function exporter_donnees_candidat( $email, $page = 1 ) {
$candidatures = get_posts( array(
'post_type' => 'candidature',
'post_status' => 'any',
'posts_per_page' => -1,
'meta_key' => '_email',
'meta_value' => $email,
) );
$elements = array();
foreach ( $candidatures as $candidature ) {
$elements[] = array(
'group_id' => 'candidatures',
'group_label' => __( 'Candidatures', 'cabinet' ),
'item_id' => 'candidature-' . $candidature->ID,
'data' => array(
array( 'name' => __( 'Nom', 'cabinet' ), 'value' => $candidature->post_title ),
array(
'name' => __( 'Consentement', 'cabinet' ),
'value' => get_post_meta( $candidature->ID, '_consentement_date', true ),
),
array(
'name' => __( 'Conservation jusqu’au', 'cabinet' ),
'value' => get_post_meta( $candidature->ID, '_date_expiration', true ),
),
),
);
}
return array(
'data' => $elements,
'done' => true,
);
}
Cette approche a un avantage décisif : l’équipe du cabinet n’a pas à apprendre un outil supplémentaire. La procédure de demande d’export déjà prévue par WordPress, avec vérification de l’adresse par courriel, sert aussi pour les candidatures. Un exporteur symétrique existe pour l’effacement, avec le filtre wp_privacy_personal_data_erasers : il permet de satisfaire une demande de suppression sans intervention manuelle dans la base.
Corriger ce qui se glisse dans les extraits précédents
Deux détails de la purge méritent une relecture avant tout déploiement réel. Le premier concerne l’état des candidatures : elles sont créées avec le statut private, et la fonction get_posts() ne renvoie par défaut que les contenus publiés. Sans 'post_status' => 'any' (ou 'private') dans la requête de purge, aucune candidature n’est jamais trouvée, et la tâche s’exécute chaque jour sans rien supprimer, sans le moindre message d’erreur. C’est le pire cas pour une obligation de suppression : tout semble en ordre.
Le second concerne les fuseaux horaires : la date d’expiration est enregistrée en temps universel avec gmdate(), alors que la comparaison utilise current_time( 'mysql' ), qui renvoie l’heure locale du site. L’écart est faible, mais il est évitable : passez true en second paramètre, current_time( 'mysql', true ), pour comparer deux dates exprimées dans la même référence. La requête ne traite par ailleurs que cinquante candidatures par passage : sur un volume plus important, la tâche rattrape son retard au fil des jours, ce qui est acceptable si l’on surveille qu’elle finit par vider la file.
Un cas concret : le cabinet face à une demande d’effacement
Un candidat écrit au cabinet pour demander la suppression de son dossier, dix mois après l’envoi de son CV. Sans mécanisme prévu, l’assistante chercherait le fichier dans la médiathèque, puis le post dans l’administration, en espérant ne rien oublier. Avec l’extension, l’effaceur branché sur les outils de confidentialité supprime le fichier, les métadonnées et la candidature en une seule opération, puis la demande est consignée dans le registre du cabinet. La demande tient en quelques minutes, et la preuve de l’effacement est conservée sans conserver la donnée elle-même.
Une obligation de suppression qui repose sur la mémoire d’une personne n’est pas une obligation respectée : c’est une obligation reportée.
Tenir un journal sans conserver la donnée
Pour prouver qu’une purge a bien eu lieu, il faut garder une trace, mais pas n’importe laquelle. Un journal utile contient l’identifiant interne, la date de suppression et le motif (expiration ou demande du candidat). Il ne contient ni le nom, ni l’adresse électronique, ni aucun élément qui permettrait de reconstituer la personne : un journal qui stocke les données effacées en recrée une copie. Une simple option, tronquée à quelques centaines d’entrées, suffit pour la plupart des cabinets.
Tester la chaîne complète
La tâche planifiée se déclenche à la demande avec WP-CLI : wp cron event run purge_candidatures_quotidienne. Créez une candidature de test avec une date d’expiration dans le passé, lancez la commande, puis vérifiez trois choses : le post a disparu, le fichier a disparu de la médiathèque et du disque, et le journal contient une ligne. Sur un hébergement où le déclencheur de WordPress dépend des visites, pensez à le remplacer par une vraie tâche système, sans quoi une purge quotidienne peut attendre plusieurs jours sur un site peu fréquenté.
Les pièges à éviter
- Promettre une durée dans la politique de confidentialité sans aucun code qui l’applique.
- Supprimer le post et oublier le fichier, ou l’inverse : la trace partielle reste une donnée personnelle.
- Sauvegarder la base avec ses candidatures expirées pour une durée illimitée : les sauvegardes entrent elles aussi dans le périmètre de la conservation.
- Prolonger un dossier sans consentement distinct et horodaté du candidat.
- Envoyer le lien vers le CV dans un courriel non protégé : préférez un lien qui exige une connexion.
Quand ne pas stocker de candidature
Si le cabinet n’a pas l’usage d’un vivier et traite les dossiers dans l’outil de suivi d’un autre prestataire, la meilleure conformité est de ne rien stocker dans WordPress : le formulaire transmet et s’efface. Conserver moins de données est toujours plus simple à défendre que de bien gérer beaucoup de données.
Conclusion
Une candidature conforme commence par un schéma de données qui connaît sa propre échéance. Le reste en découle : une purge quotidienne vérifiable, un consentement horodaté, un export et un effacement branchés sur les outils du cœur, et un journal qui prouve sans exposer. Chacune de ces pièces est courte ; ensemble, elles remplacent un engagement de principe par un comportement que l’on peut tester.