vendredi 25 septembre 2026

À propos

Contact

Extensions

map_meta_cap : des capacités personnalisées qui varient selon le contenu ciblé

current_user_can('edit_post', 42) ne répond jamais à la même question selon l'article visé. Voici ce qui se passe réellement derrière cet appel.

Par Clément Hadrot • 12 décembre 2023 • 5 min de lecture • Aucun commentaire
map_meta_cap : des capacités personnalisées qui varient selon le contenu ciblé

Un client nous a signalé un bug étrange sur son intranet : certains rédacteurs pouvaient modifier des articles qui ne leur appartenaient pas, alors que le code vérifiait pourtant current_user_can('edit_post', $post_id) avant chaque enregistrement. La vérification était bien là, correctement écrite. Le problème venait d’ailleurs : de la façon dont WordPress transforme cette capacité apparente en autorisations réelles.

C’est là qu’intervient map_meta_cap, l’un des mécanismes les plus mal compris de WordPress. Il ne s’agit pas d’une capacité stockée en base auprès de chaque utilisateur, mais d’une traduction dynamique qui dépend du contenu ciblé, de son auteur et de son statut. Comprendre cette mécanique évite des failles de sécurité discrètes et des bugs de permissions difficiles à reproduire.

Capacités primitives contre capacités méta

WordPress distingue deux familles de capacités. Les capacités primitives, comme edit_posts ou edit_others_posts, sont celles réellement stockées dans la table wp_usermeta, associées à un rôle. Les capacités méta, comme edit_post, delete_post ou read_post, n’existent nulle part en base : ce sont des alias contextuels qui n’ont de sens qu’appliqués à un objet précis, identifié par son ID.

Quand le code appelle current_user_can('edit_post', 42), WordPress ne cherche jamais une capacité littéralement nommée edit_post dans le profil de l’utilisateur. La fonction map_meta_cap() intercepte l’appel et le traduit en une ou plusieurs capacités primitives, en fonction de l’article 42 : qui l’a écrit, quel est son statut, à quel type de contenu il appartient.

Ce que fait réellement la traduction

Pour un article standard, la logique interne de map_meta_cap() ressemble à ceci : si l’utilisateur est l’auteur de l’article, la capacité requise devient edit_posts ; s’il n’en est pas l’auteur, elle devient edit_others_posts ; si l’article est publié, s’ajoute edit_published_posts ; s’il est en attente de relecture, la logique diffère encore selon les réglages du type de contenu.

L'essentiel à retenir : Une capacité méta n'existe pas telle quelle en base ; Le filtre map_meta_cap traduit selon le contexte ; Un CPT mal configuré casse cette traduction

C’est cette cascade de conditions qui explique pourquoi deux appels identiques en apparence, current_user_can('edit_post', 12) et current_user_can('edit_post', 99), peuvent renvoyer des résultats opposés pour le même utilisateur : tout dépend du contenu visé, pas de la capacité nommée.

Le piège des types de contenu personnalisés

Le bug de notre client venait d’une extension tierce enregistrant un type de contenu personnalisé avec 'capability_type' => 'post' mais 'map_meta_cap' => false. Résultat : WordPress ne traduisait plus edit_post en capacités primitives contextuelles, il retombait sur une vérification simplifiée qui ignorait la notion d’auteur. Tout utilisateur ayant edit_posts pouvait alors modifier n’importe quel contenu de ce type, y compris ceux des autres.

register_post_type( 'dossier_client', array(
    'capability_type' => 'dossier_client',
    'map_meta_cap'    => true, // indispensable pour une traduction correcte
    'capabilities'    => array(
        'edit_post'          => 'edit_dossier_client',
        'read_post'          => 'read_dossier_client',
        'delete_post'        => 'delete_dossier_client',
        'edit_posts'         => 'edit_dossier_clients',
        'edit_others_posts'  => 'edit_others_dossier_clients',
        'publish_posts'      => 'publish_dossier_clients',
        'read_private_posts' => 'read_private_dossier_clients',
    ),
) );

Sans map_meta_cap activé à true, les capacités méta déclarées dans le tableau capabilities ne sont jamais résolues en capacités primitives contextuelles : WordPress compare directement la chaîne à ce que possède l’utilisateur, sans tenir compte de qui a créé le contenu.

Écrire son propre filtre map_meta_cap

Il est possible d’aller plus loin que la traduction par défaut en accrochant le filtre du même nom, par exemple pour restreindre l’édition d’un dossier client à son créateur et aux administrateurs, quel que soit le rôle générique de l’utilisateur :

add_filter( 'map_meta_cap', function( $caps, $cap, $user_id, $args ) {
    if ( 'edit_dossier_client' !== $cap || empty( $args[0] ) ) {
        return $caps;
    }

    $post = get_post( $args[0] );
    if ( ! $post ) {
        return $caps;
    }

    if ( (int) $post->post_author === (int) $user_id ) {
        return array( 'edit_dossier_clients' );
    }

    return array( 'manage_options' );
}, 10, 4 );

Ce filtre remplace intégralement la liste des capacités requises. C’est un point souvent mal compris : on ne complète pas la traduction par défaut, on la remplace, sauf à appeler la fonction native au préalable et à fusionner les résultats soi-même.

Vérifier la traduction en conditions réelles

  • Utiliser user_can( $user_id, 'edit_post', $post_id ) plutôt que current_user_can() lors des tests automatisés, pour cibler un utilisateur précis sans changer le contexte global.
  • Inspecter le résultat de map_meta_cap( 'edit_post', $user_id, $post_id ) directement dans une commande WP-CLI shell pour voir la liste des capacités primitives réellement exigées.
  • Se méfier des extensions qui déclarent 'capability_type' => 'post' sans le vouloir : cela mélange les permissions du nouveau type de contenu avec celles des articles standards.

Sur nos projets, la première chose que nous vérifions face à un bug de permissions n’est jamais le rôle de l’utilisateur, mais la déclaration capability_type du contenu concerné. C’est là que se cachent neuf pièges sur dix.

En résumé

La capacité edit_post n’a jamais de valeur fixe : c’est un contrat dont la traduction dépend entièrement du contenu ciblé. Ignorer cette mécanique, ou désactiver map_meta_cap par accident sur un type de contenu personnalisé, ouvre la porte à des permissions bien plus larges que ce que le code laisse penser. Avant de blâmer un rôle utilisateur mal configuré, il vaut mieux vérifier ce que map_meta_cap() a réellement produit pour l’objet en question.

Partager :

À propos de l'auteur

Clément Hadrot

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi