Skip to content
60 changes: 52 additions & 8 deletions src/wp-includes/capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@
* `edit_app_password`, `delete_app_passwords`, `delete_app_password`,
* and `update_https` capabilities.
* @since 6.7.0 Added the `edit_block_binding` capability.
* @since 7.1.0 Added the `delete_comment` and `moderate_comment` meta capabilities,
* support for registered comment type capabilities in `edit_comment`,
* and translation of custom comment type meta capabilities.
*
* @global array $post_type_meta_caps Used to get post type meta capabilities.
* @global array $post_type_meta_caps Used to get post type meta capabilities.
* @global array $comment_type_meta_caps Used to get comment type meta capabilities.
*
* @param string $cap Capability being checked.
* @param int $user_id User ID.
Expand Down Expand Up @@ -552,14 +556,16 @@ function map_meta_cap( $cap, $user_id, ...$args ) {
}
break;
case 'edit_comment':
case 'delete_comment':
case 'moderate_comment':
if ( ! isset( $args[0] ) ) {
/* translators: %s: Capability name. */
$message = __( 'When checking for the %s capability, you must always check it against a specific comment.' );

_doing_it_wrong(
__FUNCTION__,
sprintf( $message, '<code>' . $cap . '</code>' ),
'6.1.0'
'edit_comment' === $cap ? '6.1.0' : '7.1.0'
);

$caps[] = 'do_not_allow';
Expand All @@ -572,16 +578,48 @@ function map_meta_cap( $cap, $user_id, ...$args ) {
break;
}

$post = get_post( $comment->comment_post_ID );
$comment_type = get_comment_type_object( $comment->comment_type );

/*
* If the post doesn't exist, we have an orphaned comment.
* Fall back to the edit_posts capability, instead.
* Moderation is gated by the type's moderation primitive, which for
* unregistered and default-model types is the global `moderate_comments`
* primitive, exactly as before.
*/
if ( $post ) {
$caps = map_meta_cap( 'edit_post', $user_id, $post->ID );
if ( 'moderate_comment' === $cap ) {
$caps[] = $comment_type ? $comment_type->cap->moderate_comments : 'moderate_comments';
break;
}

/*
* Comment types using the default 'comment' capability model derive edit
* and delete permission from the comment's parent post, preserving
* historical behavior. Overriding a singular meta capability (via
* 'capability_type' or 'capabilities') opts that action into the
* independent model: the type's own plural primitives gate it instead,
* mirroring how registered post types map `edit_post`. The independent
* model deliberately does not consult the parent post, so a trashed or
* private parent does not affect who can act on such comments.
*/
$uses_default_model = ! $comment_type || $cap === $comment_type->cap->$cap;

if ( $uses_default_model ) {
$post = get_post( $comment->comment_post_ID );

/*
* If the post doesn't exist, we have an orphaned comment.
* Fall back to the edit_posts capability, instead.
*/
if ( $post ) {
$caps = map_meta_cap( 'edit_post', $user_id, $post->ID );
} else {
$caps = map_meta_cap( 'edit_posts', $user_id );
}
} elseif ( 'delete_comment' === $cap ) {
$caps[] = $comment_type->cap->delete_comments;
} elseif ( $comment->user_id && (int) $comment->user_id === (int) $user_id ) {
$caps[] = $comment_type->cap->edit_comments;
} else {
$caps = map_meta_cap( 'edit_posts', $user_id );
$caps[] = $comment_type->cap->edit_others_comments;
}
break;
case 'unfiltered_upload':
Expand Down Expand Up @@ -843,6 +881,12 @@ function map_meta_cap( $cap, $user_id, ...$args ) {
return map_meta_cap( $post_type_meta_caps[ $cap ], $user_id, ...$args );
}

// Handle meta capabilities for custom comment types.
global $comment_type_meta_caps;
if ( isset( $comment_type_meta_caps[ $cap ] ) ) {
return map_meta_cap( $comment_type_meta_caps[ $cap ], $user_id, ...$args );
}

// Block capabilities map to their post equivalent.
$block_caps = array(
'edit_blocks',
Expand Down
10 changes: 6 additions & 4 deletions src/wp-includes/class-wp-comment-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,12 @@ final class WP_Comment_Type {
* Capabilities for this comment type.
*
* Built by {@see get_comment_type_capabilities()} from the
* `capability_type` and `capabilities` arguments. This is advisory metadata
* describing the capabilities associated with the comment type; the
* capability mapping in {@see map_meta_cap()} is not affected by this
* property in this release.
* `capability_type` and `capabilities` arguments. Types using the default
* capability model derive edit and delete permission from the comment's
* parent post and moderation from the global `moderate_comments`
* capability. A type that overrides a singular meta capability is instead
* gated by its own plural primitive capabilities in {@see map_meta_cap()},
* which the plugin registering the type must grant to roles.
*
* @since 7.1.0
* @var stdClass
Expand Down
86 changes: 80 additions & 6 deletions src/wp-includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -469,13 +469,14 @@ function register_comment_type( $comment_type, $args = array() ) {
*
* @since 7.1.0
*
* @global WP_Comment_Type[] $wp_comment_types List of comment types.
* @global WP_Comment_Type[] $wp_comment_types List of comment types.
* @global array $comment_type_meta_caps Used to remove meta capabilities.
*
* @param string $comment_type Comment type key.
* @return true|WP_Error True on success, WP_Error on failure or if the comment type doesn't exist.
*/
function unregister_comment_type( $comment_type ) {
global $wp_comment_types;
global $wp_comment_types, $comment_type_meta_caps;

if ( ! comment_type_exists( $comment_type ) ) {
return new WP_Error( 'invalid_comment_type', __( 'Invalid comment type.' ) );
Expand All @@ -488,6 +489,11 @@ function unregister_comment_type( $comment_type ) {
return new WP_Error( 'invalid_comment_type', __( 'Unregistering a built-in comment type is not allowed.' ) );
}

// Remove custom meta capabilities registered for this comment type.
foreach ( (array) $comment_type_object->cap as $custom_cap ) {
unset( $comment_type_meta_caps[ $custom_cap ] );
}

unset( $wp_comment_types[ $comment_type ] );

/**
Expand Down Expand Up @@ -629,15 +635,31 @@ function get_comment_type_labels( $comment_type_object ) {
* Comment type capabilities use the `capability_type` argument as a base, if
* the capability is not set in the `capabilities` argument.
*
* This is advisory metadata describing the capabilities associated with a comment
* type, modeled on {@see get_post_type_capabilities()}. The capability mapping in
* {@see map_meta_cap()} is not affected by these capabilities in this release.
* This is modeled on {@see get_post_type_capabilities()}. By default the
* capability model is the historical one: edit and delete permission for a
* comment derives from the comment's parent post, and moderation requires the
* global `moderate_comments` capability. Overriding a singular meta capability -
* for example registering with `capability_type => 'review'`, or passing a
* custom `edit_comment` name in `capabilities` - opts that action into the
* independent model: {@see map_meta_cap()} then requires the type's
* corresponding plural primitive capabilities (e.g. `edit_reviews`,
* `edit_others_reviews`), which exist in no role by default. As with custom
* post type capabilities, the plugin registering the type must grant those
* primitives to roles, typically on activation.
*
* The capability strings are built from the `capability_type` argument, which may
* be a string or an array. When a string, the plural is created by appending an
* 's'. When an array, the first element is the singular base and the second the
* plural base, e.g. array( 'story', 'stories' ).
*
* Warning: The two models can mix per action. Registering with the default
* `capability_type` of 'comment' but a `capabilities` array of
* `array( 'edit_comment' => 'edit_my_comment' )` flips only editing to the
* independent model, gated by the generated `edit_comments` primitive - which
* no default role has. Similarly, an empty `capability_type` produces
* capabilities such as `edit_` and `edit_s` that lock everyone out; the value
* is not validated, matching register_post_type().
*
* Note: With the default `capability_type` of 'comment', most of the generated
* primitive capabilities (`edit_comments`, `edit_others_comments`,
* `delete_comments`) exist in no default role and are not consulted by the
Expand Down Expand Up @@ -698,7 +720,59 @@ function get_comment_type_capabilities( $args ) {

$capabilities = array_merge( $default_capabilities, $args->capabilities );

return (object) $capabilities;
$capabilities = (object) $capabilities;

// Remember custom meta capabilities so map_meta_cap() can translate them.
_comment_type_meta_capabilities( $capabilities );

return $capabilities;
}

/**
* Stores a list of comment type meta capabilities for map_meta_cap().
*
* Only the singular meta capabilities that map_meta_cap() can translate are
* remembered: `edit_comment`, `delete_comment`, and `moderate_comment`.
* `read_comment` is excluded until a corresponding case exists in
* map_meta_cap(). Identity mappings (a custom name equal to the generic name)
* are skipped, as are names already registered as post type meta capabilities,
* which take precedence in map_meta_cap().
*
* @since 7.1.0
* @access private
*
* @global array $comment_type_meta_caps Used to store comment type meta capabilities.
* @global array $post_type_meta_caps Post type meta capabilities, checked for collisions.
*
* @param object $capabilities Comment type capabilities object.
*/
function _comment_type_meta_capabilities( $capabilities ) {
global $comment_type_meta_caps, $post_type_meta_caps;

foreach ( (array) $capabilities as $core => $custom ) {
if ( ! in_array( $core, array( 'edit_comment', 'delete_comment', 'moderate_comment' ), true ) ) {
continue;
}

if ( $core === $custom ) {
continue;
}

if ( isset( $post_type_meta_caps[ $custom ] ) ) {
_doing_it_wrong(
'register_comment_type',
sprintf(
/* translators: %s: Capability name. */
__( 'The meta capability "%s" is already registered for a post type and cannot be reused for a comment type.' ),
$custom
),
'7.1.0'
);
continue;
}

$comment_type_meta_caps[ $custom ] = $core;
}
}

/**
Expand Down
4 changes: 4 additions & 0 deletions tests/phpunit/includes/abstract-testcase.php
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ protected function reset_taxonomies() {
* it has a chance to do so.
*/
protected function reset_comment_types() {
// Direct get_comment_type_capabilities() calls register translations
// without a comment type to unregister, so reset the registry wholesale.
$GLOBALS['comment_type_meta_caps'] = array();

foreach ( get_comment_types( array( '_builtin' => false ) ) as $comment_type ) {
_unregister_comment_type( $comment_type );
}
Expand Down
Loading
Loading