Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 42 additions & 8 deletions src/wp-includes/capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@
* `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.3.0 Added the `delete_comment` and `moderate_comment` meta capabilities
* and support for registered comment type 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<string, string> $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 +555,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.3.0'
);

$caps[] = 'do_not_allow';
Expand All @@ -572,16 +577,39 @@ 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 );

// Unregistered and default-model types fall back to `moderate_comments`.
if ( 'moderate_comment' === $cap ) {
$caps[] = $comment_type ? $comment_type->cap->moderate_comments : 'moderate_comments';
break;
}

/*
* If the post doesn't exist, we have an orphaned comment.
* Fall back to the edit_posts capability, instead.
* A type that overrides this meta capability is gated by its own plural
* primitives, without consulting the parent post. Otherwise permission
* derives from the parent post.
*/
if ( $post ) {
$caps = map_meta_cap( 'edit_post', $user_id, $post->ID );
$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 @@ -842,6 +870,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
3 changes: 1 addition & 2 deletions src/wp-includes/class-wp-comment-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,7 @@ final class WP_Comment_Type {
/**
* Capabilities for this comment type.
*
* Advisory only; map_meta_cap() does not consult them yet.
* See get_comment_type_capabilities().
* Built by get_comment_type_capabilities() and enforced by map_meta_cap().
*
* @since 7.3.0
* @var stdClass
Expand Down
65 changes: 61 additions & 4 deletions src/wp-includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -553,13 +553,14 @@ function register_comment_type( $comment_type, $args = array() ) {
*
* @since 7.3.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<string, string> $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;

$comment_type_object = get_comment_type_object( $comment_type );

Expand All @@ -572,6 +573,12 @@ function unregister_comment_type( $comment_type ) {
return new WP_Error( 'invalid_comment_type', __( 'Unregistering a built-in comment type is not allowed.' ) );
}

foreach ( (array) $comment_type_object->cap as $custom_cap ) {
if ( is_string( $custom_cap ) ) {
unset( $comment_type_meta_caps[ $custom_cap ] );
}
}

unset( $wp_comment_types[ $comment_type ] );

/**
Expand Down Expand Up @@ -714,13 +721,20 @@ function get_comment_type_labels( $comment_type_object ) {
* `capability_type` argument. A string base is pluralized by appending an 's';
* an array supplies the singular and plural bases, e.g. array( 'story', 'stories' ).
*
* The capabilities are advisory: map_meta_cap() does not consult them yet, and
* most of the generated names are not granted to any default role.
* By default, editing and deleting a comment derive from its parent post, and
* moderating requires `moderate_comments`. When a type overrides a singular meta
* capability, for example with `capability_type => 'review'`, map_meta_cap()
* instead requires the matching plural primitive (e.g. `edit_reviews`). No role
* has these by default; the plugin registering the type must grant them.
*
* Do not reuse a post type's `capability_type` as the base. A base of 'post'
* generates `edit_comment => 'edit_post'`, which map_meta_cap() resolves
* against a post with the comment's ID.
*
* The choice is per action: overriding only `edit_comment` gates editing by
* the type's `edit_comments` primitive while deletion still follows the parent
* post. The "others" axis is edit-only: there is no `delete_others_comments`.
*
* Normalizes the `capability_type` property of `$args` to its array form.
*
* @since 7.3.0
Expand Down Expand Up @@ -768,9 +782,52 @@ function get_comment_type_capabilities( object $args ): stdClass {

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

_comment_type_meta_capabilities( $capabilities );

return (object) $capabilities;
}

/**
* Stores custom comment type meta capability names for map_meta_cap().
*
* Only `edit_comment`, `delete_comment`, and `moderate_comment` are stored.
* Names already registered as post type meta capabilities are skipped.
*
* @since 7.3.0
* @access private
*
* @global array<string, string> $comment_type_meta_caps Used to store comment type meta capabilities.
* @global array<string, string> $post_type_meta_caps Post type meta capabilities, checked for collisions.
*
* @param array<mixed> $capabilities Comment type capabilities, keyed by generic capability name.
*/
function _comment_type_meta_capabilities( array $capabilities ): void {
global $comment_type_meta_caps, $post_type_meta_caps;

foreach ( array( 'edit_comment', 'delete_comment', 'moderate_comment' ) as $core ) {
$custom = $capabilities[ $core ] ?? null;

if ( ! is_string( $custom ) || $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.3.0'
);
continue;
}

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

/**
* Retrieves all of the WordPress supported comment statuses.
*
Expand Down
4 changes: 3 additions & 1 deletion tests/phpunit/includes/abstract-testcase.php
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,9 @@ protected function reset_taxonomies() {
* cannot be unregistered.
*/
protected function reset_comment_types(): void {
$GLOBALS['wp_comment_types'] = array();
// get_comment_type_capabilities() can store translations without a registered type.
$GLOBALS['comment_type_meta_caps'] = array();
$GLOBALS['wp_comment_types'] = array();

create_initial_comment_types();
}
Expand Down
Loading