* Core User Role & Capabilities API
* Maps a capability to the primitive capabilities required of the given user to
* satisfy the capability being checked.
* This function also accepts an ID of an object to map against if the capability is a meta capability. Meta
* capabilities such as `edit_post` and `edit_user` are capabilities used by this function to map to primitive
* capabilities that a user or role requires, such as `edit_posts` and `edit_others_posts`.
* map_meta_cap( 'edit_posts', $user->ID );
* map_meta_cap( 'edit_post', $user->ID, $post->ID );
* map_meta_cap( 'edit_post_meta', $user->ID, $post->ID, $meta_key );
* This function does not check whether the user has the required capabilities,
* it just returns what the required capabilities are.
* @since 4.9.6 Added the `export_others_personal_data`, `erase_others_personal_data`,
* and `manage_privacy_options` capabilities.
* @since 5.1.0 Added the `update_php` capability.
* @since 5.2.0 Added the `resume_plugin` and `resume_theme` capabilities.
* @since 5.3.0 Formalized the existing and already documented `...$args` parameter
* by adding it to the function signature.
* @since 5.7.0 Added the `create_app_password`, `list_app_passwords`, `read_app_password`,
* `edit_app_password`, `delete_app_passwords`, `delete_app_password`,
* and `update_https` capabilities.
* @since 6.7.0 Added the `edit_block_binding` capability.
* @global array $post_type_meta_caps Used to get post type meta capabilities.
* @param string $cap Capability being checked.
* @param int $user_id User ID.
* @param mixed ...$args Optional further parameters, typically starting with an object ID.
* @return string[] Primitive capabilities required of the user.
function map_meta_cap( $cap, $user_id, ...$args ) {
// In multisite the user must be a super admin to remove themselves.
if ( isset( $args[0] ) && $user_id === (int) $args[0] && ! is_super_admin( $user_id ) ) {
$caps[] = 'do_not_allow';
$caps[] = 'remove_users';
$caps[] = 'promote_users';
// Allow user to edit themselves.
if ( 'edit_user' === $cap && isset( $args[0] ) && $user_id === (int) $args[0] ) {
// In multisite the user must have manage_network_users caps. If editing a super admin, the user must be a super admin.
if ( is_multisite() && ( ( ! is_super_admin( $user_id ) && 'edit_user' === $cap && is_super_admin( $args[0] ) ) || ! user_can( $user_id, 'manage_network_users' ) ) ) {
$caps[] = 'do_not_allow';
$caps[] = 'edit_users'; // edit_user maps to edit_users.
if ( ! isset( $args[0] ) ) {
if ( 'delete_post' === $cap ) {
/* translators: %s: Capability name. */
$message = __( 'When checking for the %s capability, you must always check it against a specific post.' );
/* translators: %s: Capability name. */
$message = __( 'When checking for the %s capability, you must always check it against a specific page.' );
sprintf( $message, '<code>' . $cap . '</code>' ),
$caps[] = 'do_not_allow';
$post = get_post( $args[0] );
$caps[] = 'do_not_allow';
if ( 'revision' === $post->post_type ) {
$caps[] = 'do_not_allow';
if ( (int) get_option( 'page_for_posts' ) === $post->ID
|| (int) get_option( 'page_on_front' ) === $post->ID
$caps[] = 'manage_options';
$post_type = get_post_type_object( $post->post_type );
/* translators: 1: Post type, 2: Capability name. */
$message = __( 'The post type %1$s is not registered, so it may not be reliable to check the capability %2$s against a post of that type.' );
'<code>' . $post->post_type . '</code>',
'<code>' . $cap . '</code>'
$caps[] = 'edit_others_posts';
if ( ! $post_type->map_meta_cap ) {
$caps[] = $post_type->cap->$cap;
// Prior to 3.1 we would re-call map_meta_cap here.
if ( 'delete_post' === $cap ) {
$cap = $post_type->cap->$cap;
// If the post author is set and the user is the author...
if ( $post->post_author && $user_id === (int) $post->post_author ) {
// If the post is published or scheduled...
if ( in_array( $post->post_status, array( 'publish', 'future' ), true ) ) {
$caps[] = $post_type->cap->delete_published_posts;
} elseif ( 'trash' === $post->post_status ) {
$status = get_post_meta( $post->ID, '_wp_trash_meta_status', true );
if ( in_array( $status, array( 'publish', 'future' ), true ) ) {
$caps[] = $post_type->cap->delete_published_posts;
$caps[] = $post_type->cap->delete_posts;
// If the post is draft...
$caps[] = $post_type->cap->delete_posts;
// The user is trying to edit someone else's post.
$caps[] = $post_type->cap->delete_others_posts;
// The post is published or scheduled, extra cap required.
if ( in_array( $post->post_status, array( 'publish', 'future' ), true ) ) {
$caps[] = $post_type->cap->delete_published_posts;
} elseif ( 'private' === $post->post_status ) {
$caps[] = $post_type->cap->delete_private_posts;
* Setting the privacy policy page requires `manage_privacy_options`,
* so deleting it should require that too.
if ( (int) get_option( 'wp_page_for_privacy_policy' ) === $post->ID ) {
$caps = array_merge( $caps, map_meta_cap( 'manage_privacy_options', $user_id ) );
* edit_post breaks down to edit_posts, edit_published_posts, or
if ( ! isset( $args[0] ) ) {
if ( 'edit_post' === $cap ) {
/* translators: %s: Capability name. */
$message = __( 'When checking for the %s capability, you must always check it against a specific post.' );
/* translators: %s: Capability name. */
$message = __( 'When checking for the %s capability, you must always check it against a specific page.' );
sprintf( $message, '<code>' . $cap . '</code>' ),
$caps[] = 'do_not_allow';
$post = get_post( $args[0] );
$caps[] = 'do_not_allow';
if ( 'revision' === $post->post_type ) {
$post = get_post( $post->post_parent );
$caps[] = 'do_not_allow';
$post_type = get_post_type_object( $post->post_type );
/* translators: 1: Post type, 2: Capability name. */
$message = __( 'The post type %1$s is not registered, so it may not be reliable to check the capability %2$s against a post of that type.' );
'<code>' . $post->post_type . '</code>',
'<code>' . $cap . '</code>'
$caps[] = 'edit_others_posts';
if ( ! $post_type->map_meta_cap ) {
$caps[] = $post_type->cap->$cap;
// Prior to 3.1 we would re-call map_meta_cap here.
if ( 'edit_post' === $cap ) {
$cap = $post_type->cap->$cap;
// If the post author is set and the user is the author...
if ( $post->post_author && $user_id === (int) $post->post_author ) {
// If the post is published or scheduled...
if ( in_array( $post->post_status, array( 'publish', 'future' ), true ) ) {
$caps[] = $post_type->cap->edit_published_posts;
} elseif ( 'trash' === $post->post_status ) {
$status = get_post_meta( $post->ID, '_wp_trash_meta_status', true );
if ( in_array( $status, array( 'publish', 'future' ), true ) ) {
$caps[] = $post_type->cap->edit_published_posts;
$caps[] = $post_type->cap->edit_posts;
// If the post is draft...
$caps[] = $post_type->cap->edit_posts;
// The user is trying to edit someone else's post.
$caps[] = $post_type->cap->edit_others_posts;
// The post is published or scheduled, extra cap required.
if ( in_array( $post->post_status, array( 'publish', 'future' ), true ) ) {
$caps[] = $post_type->cap->edit_published_posts;
} elseif ( 'private' === $post->post_status ) {
$caps[] = $post_type->cap->edit_private_posts;
* Setting the privacy policy page requires `manage_privacy_options`,
* so editing it should require that too.
if ( (int) get_option( 'wp_page_for_privacy_policy' ) === $post->ID ) {
$caps = array_merge( $caps, map_meta_cap( 'manage_privacy_options', $user_id ) );
if ( ! isset( $args[0] ) ) {
if ( 'read_post' === $cap ) {
/* translators: %s: Capability name. */
$message = __( 'When checking for the %s capability, you must always check it against a specific post.' );
/* translators: %s: Capability name. */
$message = __( 'When checking for the %s capability, you must always check it against a specific page.' );
sprintf( $message, '<code>' . $cap . '</code>' ),
$caps[] = 'do_not_allow';
$post = get_post( $args[0] );
$caps[] = 'do_not_allow';
if ( 'revision' === $post->post_type ) {
$post = get_post( $post->post_parent );
$caps[] = 'do_not_allow';
$post_type = get_post_type_object( $post->post_type );
/* translators: 1: Post type, 2: Capability name. */
$message = __( 'The post type %1$s is not registered, so it may not be reliable to check the capability %2$s against a post of that type.' );
'<code>' . $post->post_type . '</code>',
'<code>' . $cap . '</code>'
$caps[] = 'edit_others_posts';
if ( ! $post_type->map_meta_cap ) {
$caps[] = $post_type->cap->$cap;
// Prior to 3.1 we would re-call map_meta_cap here.
if ( 'read_post' === $cap ) {
$cap = $post_type->cap->$cap;
$status_obj = get_post_status_object( get_post_status( $post ) );
/* translators: 1: Post status, 2: Capability name. */
$message = __( 'The post status %1$s is not registered, so it may not be reliable to check the capability %2$s against a post with that status.' );
'<code>' . get_post_status( $post ) . '</code>',
'<code>' . $cap . '</code>'
$caps[] = 'edit_others_posts';
if ( $status_obj->public ) {
$caps[] = $post_type->cap->read;
if ( $post->post_author && $user_id === (int) $post->post_author ) {
$caps[] = $post_type->cap->read;
} elseif ( $status_obj->private ) {
$caps[] = $post_type->cap->read_private_posts;
$caps = map_meta_cap( 'edit_post', $user_id, $post->ID );
if ( ! isset( $args[0] ) ) {
/* translators: %s: Capability name. */
$message = __( 'When checking for the %s capability, you must always check it against a specific post.' );
sprintf( $message, '<code>' . $cap . '</code>' ),
$caps[] = 'do_not_allow';
$post = get_post( $args[0] );
$caps[] = 'do_not_allow';
$post_type = get_post_type_object( $post->post_type );
/* translators: 1: Post type, 2: Capability name. */
$message = __( 'The post type %1$s is not registered, so it may not be reliable to check the capability %2$s against a post of that type.' );
'<code>' . $post->post_type . '</code>',
'<code>' . $cap . '</code>'
$caps[] = 'edit_others_posts';
$caps[] = $post_type->cap->publish_posts;
case 'edit_comment_meta':
case 'delete_comment_meta':
$object_type = explode( '_', $cap )[1];
if ( ! isset( $args[0] ) ) {
if ( 'post' === $object_type ) {
/* translators: %s: Capability name. */
$message = __( 'When checking for the %s capability, you must always check it against a specific post.' );
} elseif ( 'comment' === $object_type ) {
/* translators: %s: Capability name. */
$message = __( 'When checking for the %s capability, you must always check it against a specific comment.' );
} elseif ( 'term' === $object_type ) {
/* translators: %s: Capability name. */
$message = __( 'When checking for the %s capability, you must always check it against a specific term.' );
/* translators: %s: Capability name. */
$message = __( 'When checking for the %s capability, you must always check it against a specific user.' );
sprintf( $message, '<code>' . $cap . '</code>' ),
$caps[] = 'do_not_allow';
$object_id = (int) $args[0];
$object_subtype = get_object_subtype( $object_type, $object_id );
if ( empty( $object_subtype ) ) {
$caps[] = 'do_not_allow';
$caps = map_meta_cap( "edit_{$object_type}", $user_id, $object_id );
$meta_key = isset( $args[1] ) ? $args[1] : false;
$allowed = ! is_protected_meta( $meta_key, $object_type );
if ( ! empty( $object_subtype ) && has_filter( "auth_{$object_type}_meta_{$meta_key}_for_{$object_subtype}" ) ) {
* Filters whether the user is allowed to edit a specific meta key of a specific object type and subtype.
* The dynamic portions of the hook name, `$object_type`, `$meta_key`,
* and `$object_subtype`, refer to the metadata object type (comment, post, term or user),
* the meta key value, and the object subtype respectively.
* @param bool $allowed Whether the user can add the object meta. Default false.
* @param string $meta_key The meta key.
* @param int $object_id Object ID.
* @param int $user_id User ID.
* @param string $cap Capability name.
* @param string[] $caps Array of the user's capabilities.
$allowed = apply_filters( "auth_{$object_type}_meta_{$meta_key}_for_{$object_subtype}", $allowed, $meta_key, $object_id, $user_id, $cap, $caps );
* Filters whether the user is allowed to edit a specific meta key of a specific object type.
* Return true to have the mapped meta caps from `edit_{$object_type}` apply.
* The dynamic portion of the hook name, `$object_type` refers to the object type being filtered.
* The dynamic portion of the hook name, `$meta_key`, refers to the meta key passed to map_meta_cap().