Nota: creo que la mejor traducción para “capabilities” en el contexto de lo que se permite o no hacer a un usuario es “competencias”, no capacidades. El término capacidad alude más bien a habilidades, no a permisos. No obstante, utilizo capacidades por ser la traducción más utilizada en la comunidad de WordPress.

Cuándo se registra un tipo de post personalizado (utilizaré en adelante CPT para abreviar), este se asocia automáticamente con las capacidades predeterminadas para el tipo de post estándar. Esto se debe a que el parámetro capability_type tiene post como valor predeterminado en la función register_post_type():

add_action( 'init', 'cyb_register_book_cpt' );
function cyb_register_book_cpt() {
    $args = [
      'public'         => true,
      'label'          => __( 'Books', 'cyb-textdomain' ),
      // "post" es el valor predeterminado, así que
      // no es necesario si es lo que queremos.
      'capability_type' => 'post'
    ];
    register_post_type( 'book', $args );
}

Cualquier usuario tendrá acceso a publicar, actualizar o borrar el CPT de acuerdo a los mismos roles y capacidades que el tipo de post estándar. Por ejemplo, un “author” podrá publicar, editar y borrar posts propios; un editor podrá además gestionar posts de otros autores, revisar posts pendientes de colaboradores y gestionar posts privados y futuros.

Si queremos asignar las mismas capacidades asociadas al tipo de post page, bastaría con establecer capablitity_type igual a page. Y lo mismo podemos hacer para asignar capacidades personalizadas. Por ejemplo, para asignar capacidades personalizadas tipo book:

add_action( 'init', 'cyb_register_book_cpt' );
function cyb_register_book_cpt() {
    $args = [
      'public'         => true,
      'label'          => __( 'Books', 'cyb-textdomain' ),
      'capability_type' => 'book'
    ];
    register_post_type( 'book', $args );
}

Pero, a diferencia de la capacidad tipo post o page, el tipo de capacidad book no existe. Tenemos que definir y mapear las capacidades de este “tipo de capacidad” para que WordPress las entienda. Y eso es lo que vamos a aprender en este tutorial.

capability_type, capabilities y map_meta_cap

Estos tres argumentos para la función register_post_type() son los que generan las capacidades de usuario para los tipos de posts. Básicamente, esto es lo que hacen:

capability_type
(string o array) (opcional) Es un string que se utiliza para construir las capacidades de lectura (read), edición (edit), publicación (publish) y borrado (delete). El valor predeterminado es post y se construyen las capacidades read_post, edit_post, etc. Si establecemos otro valor, por ejemplo, book, las capacidades construidas por WordPress serán read_book, edit_book, publish_books, ….
Fíjate que capability_type es book pero hay capacidades que utilizan books, en plural. La s adicional es añadida automáticamente por WordPress. Si el plural construido de esta forma es erróneo, puedes utilizar un array para especificar el singular y plural que se deben utilizar. Por ejemplo:
$args = [
  'capability_type' => [ 'automovil', 'automoviles' ];
  // si utilizamos esto:
  // 'capability_type' => 'automovil',
  // WordPress utilizaría 'automovils' como plural
];
De esta forma se construyen 7 capacidades (fíjate, una vez más, en los sufijos _book y _books):
'capabilities' => [
  'edit_post'          => 'edit_book', 
  'read_post'          => 'read_book', 
  'delete_post'        => 'delete_book', 
  'edit_posts'         => 'edit_books', 
  'edit_others_posts'   => 'edit_others_books', 
  'publish_posts'      => 'publish_books',       
  'read_private_posts'  => 'read_private_books',
  // ATENCIÓN aqui. Fíjate que create_posts se hace igual
  // que edit_books. Si quieres separar ambas capacidades
  // hay que hacerlo manualmente en el argumento capabilities
  'create_posts'       => 'edit_books',
];

Estas capacidades construidas automáticamente serán las utilizadas por WordPress para otorgar o no acceso a los usuarios a las acciones asociadas, excepto si se establecen otras capacidades en el parámetro capabilities.

capabilities
(array) (opcional) Si no se establece nada aquí, se utilizan las capacidades construidas por WordPress descritas anteriormente. El array acepta las mismas 7 claves que el array que vimos antes:
// Estas capacidades no serían necesarias establecerlas ya
// que serían iguales a las generadas automáticamente
// por WordPress para capability_type => 'book'
'capabilities' => [
  'edit_post'          => 'edit_book', 
  'read_post'          => 'read_book', 
  'delete_post'        => 'delete_book', 
  'edit_posts'         => 'edit_books', 
  'edit_others_posts'   => 'edit_others_books', 
  'publish_posts'      => 'publish_books',       
  'read_private_posts'  => 'read_private_books',
  'create_posts'       => 'edit_books',
];

Por poner un ejemplo, podríamos querer que la capacidad edit_others_posts fuera manage_stock:

'capabilities' => [
  'edit_others_posts' => 'manage_stock',
];

En pocas palabras, si queremos capacidades personalizadas para nuestro tipo de post personalizado, las definimos en el argumento capabilities.

A tener en cuenta

Las 3 capacidades en singular, read_post, edit_post y delete_post, se conocen como meta capabilities y nunca deben asignarse directamente a un usuario o a un rol. Si un usuario puede “leer un post”, necesariamente tenemos que saber de que post se trata, ¿no?.
WordPress utiliza estas capacidades post a post individualmente. Por ejemplo, current_user_can( 'edit_post', 854 ) comprueba si el usuario actual puede editar el post 854, lo cual será cierto si tiene la capacidad edit_posts y es el autor del post 854 o si tiene la capacidad edit_others_posts.
Las otras 4 de las 7 capacidades mencionadas se conocen como capacidades primitivas y son: edit_posts, edit_others_posts, publish_posts y read_private_posts.
Esto puede ser un poco lioso, de momento quedaros con dos cosas claras: las capacidades meta se comprueban post a post y se mapean a sus correspondientes capacidades primitivas como vimos un par de párrafos arriba; current_user_can( 'edit_post', 854 ) era mapeado finalmente a edit_posts o edit_others_posts. Las capacidades primitivas se pueden asignar directamente a usuarios y a roles, las capacidades meta no.
map_meta_cap
(bool) (opcional). Predeterminado null. Si se establece en true, WordPress realiza el mapeo de capacidades meta a capacidades primitivas que mencionábamos antes utilizando la función map_meta_cap(). Esta función utiliza otras 8 capacidades primitivas.
En total, hay 15 capacidades. 3 meta + 7 primitivas, más otras 8 primitivas generadas en map_meta_cap():
[cap] => stdClass Object
(
	// Capacidades meta
	[edit_post]		 => "edit_{$capability_type}"
	[read_post]		 => "read_{$capability_type}"
	[delete_post]		 => "delete_{$capability_type}"

	// Capacidades primitivas utilizadas fuera de map_meta_cap():
	[edit_posts]		 => "edit_{$capability_type}s"
	[edit_others_posts]	 => "edit_others_{$capability_type}s"
	[publish_posts]		 => "publish_{$capability_type}s"
	[read_private_posts]	 => "read_private_{$capability_type}s"

	// Capacidades primitivas utilizandas en map_meta_cap():
	[read]                   => "read",
	[delete_posts]           => "delete_{$capability_type}s"
	[delete_private_posts]   => "delete_private_{$capability_type}s"
	[delete_published_posts] => "delete_published_{$capability_type}s"
	[delete_others_posts]    => "delete_others_{$capability_type}s"
	[edit_private_posts]     => "edit_private_{$capability_type}s"
	[edit_published_posts]   => "edit_published_{$capability_type}s"
	[create_posts]           => "edit_{$capability_type}s"
)

Si el argumento map_meta_cap es false o null, tenemos que definir una función propia para mapear las capaciades meta hasta capacidades primitivas. Para ello se utiliza el filtro map_meta_cap:

add_filter( 'map_meta_cap', 'cyb_map_meta_cap', 10, 4 );
function cyb_map_meta_cap( $caps, $cap, $user_id, $args ) {

	// Si es una capacidad meta tipo "book",
       // obtener el post y el objeto del post type
       // que se utilizará más adelante
	if ( 'edit_book' == $cap || 'delete_book' == $cap || 'read_book' == $cap ) {
		$post = get_post( $args[0] );
		$post_type = get_post_type_object( $post->post_type );
		// vaciar $caps
		$caps = array();
	}

	// Si se va a editar un "book", asignar las capacidades correspondientes
	if ( 'edit_book' == $cap ) {
		if ( $user_id == $post->post_author ) {
                     // Si el usuario es el autor del post,
                     // comprobar si el usuario tiene la capaciad edit_posts
			$caps[] = $post_type->cap->edit_posts;
		} else {
                     // Si el usuario no es el autor del post,
                     // comprobar si el usuario tiene la capacidad edit_others_posts
			$caps[] = $post_type->cap->edit_others_posts;
              }
	}

	// Si se va a borrar un book
	if ( 'delete_book' == $cap ) {
		if ( $user_id == $post->post_author ) {
			$caps[] = $post_type->cap->delete_posts;
		} else {
			$caps[] = $post_type->cap->delete_others_posts;
              }
	}

	// Si se va a leer un book privado
	if ( 'read_book' == $cap ) {
		if ( 'private' != $post->post_status ) {
			$caps[] = 'read';
		} elseif ( $user_id == $post->post_author ) {
			$caps[] = 'read';
		} else {
			$caps[] = $post_type->cap->read_private_posts;
              }
	}

	// Devolver las capacidades que debe tener el usuario
	return $caps;

}

Esto del mapeo de capacidades meta puede parecer muy engorroso pero permite que no tengamos que comprobar cosntantemente quien es el usuario y que capacidades tiene y cual es el post y cuales es su estado. Ya lo hace la función de mapeo y así nos podemos centrar en comprobar capacidades.

La verdad es que la mayoría de la mayoría de veces querreis utilizar map_meta_cap => true y dejar que WordPress que se encargue de ello. Las necesidades tienen que ser muy particulares para querer encargarse uno mismo del mapeo.

Asignar las capacidades a roles de usuario

Bien, ya tenemos un CPT y las capacidades personalizadas asociadas. Ya solo nos queda asignar estas capacidades a los roles de usuario deseados. Esta es la parte más sencilla. Basta utilizar el método WP_Role::add_cap.

Hay que tener en cuenta que añadir una capacidad a un rol de usuario guarda la información en la base de datos, por lo que es necesario realizarlo solo una vez para evitar operaciones innecesarias. Generalmente se hace durante la activación del plugin. Igualmente, tendremos que eliminar las capacidades añadidas durante la desactivación del plugin.

Ejemplo

Vamos a registrar el tipo de post book con capacidad tipo book y vamos a permitir que los usuarios de rol contributor puedan crear, editar y borrar books, aunque no publicarlos, ni editarlos o borrarlos si ya están publicados:

add_action( 'init', 'cyb_register_book_cpt' );
function cyb_register_book_cpt() {
    $args = [
      'public'         => true,
      'label'          => __( 'Books', 'cyb-textdomain' ),
      'capability_type' => 'book',
      'map_meta_cap'    => true
    ];
    register_post_type( 'book', $args );
}

// Asignar las capacidades adecuadas a cada role
// durante la activación del plugin
register_activation_hook( __FILE__, function () {
	
    $contributor = get_role('contributor');
    $contributor_caps = [
        'delete_books',
        // create_books es igual a edit_books de forma predeterminada
        'edit_books',
    ];
	
    foreach( $contributor_caps as $cap ) {
        $contributor->add_cap( $cap );
    }
	
    $author = get_role( 'author' );
	
    $author_caps = [
        'delete_books',
        'delete_published_books',
        'edit_books',
        'edit_published_books',
        'publish_books',
    ];
	
    foreach( $author_caps as $cap ) {
       $author->add_cap( $cap );
    }

    $editor = get_role( 'editor' );
    $administrator = get_role( 'administrator' );
	
    $editor_and_admin_caps = [
        'delete_books',
        'delete_others_books',
        'delete_private_books',
        'delete_published_books',
        'edit_books',
        'edit_others_books',
        'edit_private_books',
        'edit_published_books',
        'publish_books',
        'read_private_books'
    ];
	
    foreach( $editor_and_admin_caps as $cap ) {
        $editor->add_cap( $cap );
        $administrator->add_cap( $cap );
    }

} );

// Al desactivar el plugin, eliminar las capacidades
// que se asignaron durante la activación
register_deactivation_hook( __FILE__, function () {
	
    $contributor = get_role( 'contributor' );
    $contributor_caps = [
        'delete_books',
        'edit_books',
    ];
	
    foreach( $contributor_caps as $cap ) {
        $contributor->remove_cap( $cap );
    }
	
    $author = get_role( 'author' );
	
    $author_caps = [
        'delete_books',
        'delete_published_books',
        'edit_books',
        'edit_published_books',
        'publish_books',
    ];
	
    foreach( $author_caps as $cap ) {
       $author->remove_cap( $cap );
    }

    $editor = get_role( 'editor' );
    $administrator = get_role( 'administrator' );
	
    $editor_and_admin_caps = [
        'delete_books',
        'delete_others_books',
        'delete_private_books',
        'delete_published_books',
        'edit_books',
        'edit_others_books',
        'edit_private_books',
        'edit_published_books',
        'publish_books',
        'read_private_books'
    ];
	
    foreach( $editor_and_admin_caps as $cap ) {
        $editor->remove_cap( $cap );
        $administrator->remove_cap( $cap );
    }

} );

Como véis, la implementación parece bastante más sencilla que toda la teoría. Pero dedicadle un poco a entender también la teoría, es muy importante saber exactamente lo que se está haciendo, recordad que estamos trabajando con permisos de usuario.

  • Excelente post sobre la gestión de capabilities de los usuarios en WordPress.

    Personalmente, tampoco veo muy clara la traducción de ‘capabilities’ a ‘capacidades’, y por ello lo que yo suelo hacer, directamente, es no traducir la palabra, y dejarla tal cual en inglés: capabilities.

    Y si hubiera que traducirla, desde luego, competencias es mucho más preciso que capacidades, o quizás también valdría ‘capacitaciones’. Está claro…

    Por otro lado, aprovecho para decirte que me encanta tu forma de redactar y explicar conceptos avanzados de WordPress. Es una paradoja un poco triste, pensar que con la cantidad tan grande de blogs de WordPress que hay por ahí, en realidad existan tan pocos centrados en temas un poco más avanzados, y en castellano, como el tuyo. Enhorabuena por ello.

    Saludos!

    • Hola @juanjo_sg:disqus, precisamente la falta de ese tipo de contenido en castellano es una de las principales razones de este blog.

      Muchísimas gracias por tus palabras!!

  • Totalmente de acuerdo. Mira que es algo fácil de implementar, eh? Pues la mayoría de desarrolladores aprovechan las capacidades de “post” y “página”, y luego no hay forma de crear un rol para controlar ese CPT fácilmente… se tiene modificar primero el CPT, añadir capacidades, y es un engorro…

    • Jejeje, pues así es @boluda:disqus. Con lo fácil que es!!

      Actualmente administro una web dónde los usuarios de rol author pueden publicar “Noticias” (utilizo el tipo post estándar), pueden crear pero no publicar “Artículos”, y no pueden hacer nada en “Lecciones” y “Términos”, estos tres últimos custom post types.

      Unas cuantas líneas y listo; WordPress se encarga de controlar los permisos de cada usuario a cada tipo de post. Es genial!!