# 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.

- Auteur : Clément Hadrot
- Publié le : 2023-12-12
- Mis à jour le : 2023-12-12
- Catégorie : Extensions
- URL : https://wpmoderne.dev.wordpress-developpement.fr/extensions/map-meta-cap-capacites-personnalisees-contenu-cible/

## L’essentiel

- 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

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.
