diff --git a/src/wp-includes/capabilities.php b/src/wp-includes/capabilities.php index c812b4a16a9bd..65997fecce539 100644 --- a/src/wp-includes/capabilities.php +++ b/src/wp-includes/capabilities.php @@ -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 $comment_type_meta_caps Used to get comment type meta capabilities. * * @param string $cap Capability being checked. * @param int $user_id User ID. @@ -552,6 +555,8 @@ 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.' ); @@ -559,7 +564,7 @@ function map_meta_cap( $cap, $user_id, ...$args ) { _doing_it_wrong( __FUNCTION__, sprintf( $message, '' . $cap . '' ), - '6.1.0' + 'edit_comment' === $cap ? '6.1.0' : '7.3.0' ); $caps[] = 'do_not_allow'; @@ -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': @@ -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', diff --git a/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php index 371d5730e0adb..cae5f23c6f3b0 100644 --- a/src/wp-includes/class-wp-comment-type.php +++ b/src/wp-includes/class-wp-comment-type.php @@ -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 diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index df2c1f6503002..d579c3fec4f09 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -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 $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 ); @@ -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 ] ); /** @@ -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 @@ -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 $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 array $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. * diff --git a/tests/phpunit/includes/abstract-testcase.php b/tests/phpunit/includes/abstract-testcase.php index 33ca1452c3c76..0426efb490165 100644 --- a/tests/phpunit/includes/abstract-testcase.php +++ b/tests/phpunit/includes/abstract-testcase.php @@ -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(); } diff --git a/tests/phpunit/tests/comment/commentCapabilities.php b/tests/phpunit/tests/comment/commentCapabilities.php new file mode 100644 index 0000000000000..01714584320ad --- /dev/null +++ b/tests/phpunit/tests/comment/commentCapabilities.php @@ -0,0 +1,473 @@ +user->create( array( 'role' => 'administrator' ) ); + self::$subscriber_id = $factory->user->create( array( 'role' => 'subscriber' ) ); + self::$post_id = $factory->post->create( array( 'post_author' => self::$admin_id ) ); + } + + public function set_up(): void { + parent::set_up(); + + // A comment type with an independent capability model. + register_comment_type( 'review', array( 'capability_type' => 'review' ) ); + } + + /** + * Creates a comment of the given type, optionally attributed to a user. + * + * @param string $comment_type Comment type slug. + * @param int $user_id Authoring user ID. Default 0 (anonymous). + * @return int The new comment ID. + */ + private function make_comment( string $comment_type = 'comment', int $user_id = 0 ): int { + return self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'comment_type' => $comment_type, + 'user_id' => $user_id, + ) + ); + } + + /* + * Default capability model: behavior must match historical core exactly. + */ + + /** + * @ticket 35214 + */ + public function test_default_edit_comment_follows_parent_post(): void { + $comment_id = $this->make_comment(); + + $this->assertTrue( user_can( self::$admin_id, 'edit_comment', $comment_id ) ); + $this->assertFalse( user_can( self::$subscriber_id, 'edit_comment', $comment_id ) ); + } + + /** + * @ticket 35214 + */ + public function test_orphaned_comment_edit_falls_back_to_edit_posts(): void { + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => 0, + 'comment_type' => 'comment', + ) + ); + + $this->assertTrue( user_can( self::$admin_id, 'edit_comment', $comment_id ) ); + $this->assertFalse( user_can( self::$subscriber_id, 'edit_comment', $comment_id ) ); + } + + /** + * @ticket 35214 + */ + public function test_default_moderate_comment_requires_moderate_comments(): void { + $comment_id = $this->make_comment(); + + $this->assertTrue( user_can( self::$admin_id, 'moderate_comment', $comment_id ) ); + $this->assertFalse( user_can( self::$subscriber_id, 'moderate_comment', $comment_id ) ); + } + + /** + * @ticket 35214 + */ + public function test_default_delete_comment_follows_parent_post(): void { + $comment_id = $this->make_comment(); + + $this->assertTrue( user_can( self::$admin_id, 'delete_comment', $comment_id ) ); + $this->assertFalse( user_can( self::$subscriber_id, 'delete_comment', $comment_id ) ); + } + + /** + * @ticket 35214 + * + * @expectedIncorrectUsage map_meta_cap + */ + public function test_moderate_comment_without_comment_id_is_denied(): void { + $this->assertFalse( user_can( self::$admin_id, 'moderate_comment' ) ); + } + + /* + * Independent capability model: gated by the type's own primitives, with no + * silent fallback to the default comment capabilities. + */ + + /** + * @ticket 35214 + */ + public function test_independent_type_moderation_requires_its_own_primitive(): void { + $review_id = $this->make_comment( 'review' ); + $comment_id = $this->make_comment( 'comment' ); + + $moderator = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) ); + $moderator->add_cap( 'moderate_reviews' ); + + // The review moderator can moderate reviews but not default comments. + $this->assertTrue( user_can( $moderator->ID, 'moderate_comment', $review_id ) ); + $this->assertFalse( user_can( $moderator->ID, 'moderate_comment', $comment_id ) ); + + // An administrator has moderate_comments but not moderate_reviews: the inverse. + $this->assertTrue( user_can( self::$admin_id, 'moderate_comment', $comment_id ) ); + $this->assertFalse( user_can( self::$admin_id, 'moderate_comment', $review_id ) ); + } + + /** + * @ticket 35214 + */ + public function test_independent_type_edit_distinguishes_own_and_others(): void { + $author = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) ); + $author->add_cap( 'edit_reviews' ); + + $own_review = $this->make_comment( 'review', $author->ID ); + $others_review = $this->make_comment( 'review', self::$admin_id ); + + // edit_reviews grants editing one's own review comments only. + $this->assertTrue( user_can( $author->ID, 'edit_comment', $own_review ) ); + $this->assertFalse( user_can( $author->ID, 'edit_comment', $others_review ) ); + + // edit_others_reviews grants editing any review comment. + $editor = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) ); + $editor->add_cap( 'edit_others_reviews' ); + + $this->assertTrue( user_can( $editor->ID, 'edit_comment', $others_review ) ); + } + + /** + * @ticket 35214 + */ + public function test_independent_type_delete_requires_its_own_primitive(): void { + $review_id = $this->make_comment( 'review' ); + + $deleter = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) ); + $deleter->add_cap( 'delete_reviews' ); + + $this->assertTrue( user_can( $deleter->ID, 'delete_comment', $review_id ) ); + $this->assertFalse( user_can( self::$subscriber_id, 'delete_comment', $review_id ) ); + } + + /** + * A comment stored with the legacy empty string type uses the default model + * for all three meta capabilities (the null type object fallback). + * + * @ticket 35214 + */ + public function test_legacy_empty_type_uses_default_model_for_all_meta_caps(): void { + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'comment_type' => '', + ) + ); + + foreach ( array( 'edit_comment', 'delete_comment', 'moderate_comment' ) as $meta_cap ) { + $this->assertTrue( user_can( self::$admin_id, $meta_cap, $comment_id ), "Admin should have {$meta_cap} on a legacy comment." ); + $this->assertFalse( user_can( self::$subscriber_id, $meta_cap, $comment_id ), "Subscriber should not have {$meta_cap} on a legacy comment." ); + } + } + + /** + * The independent model never consults the parent post: an orphaned comment + * of an independent type is fully controlled by the type's primitives. + * + * @ticket 35214 + */ + public function test_independent_type_ignores_parent_post_entirely(): void { + $orphan_review = self::factory()->comment->create( + array( + 'comment_post_ID' => 0, + 'comment_type' => 'review', + ) + ); + + $power_user = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) ); + $power_user->add_cap( 'edit_others_reviews' ); + $power_user->add_cap( 'delete_reviews' ); + $power_user->add_cap( 'moderate_reviews' ); + + $this->assertTrue( user_can( $power_user->ID, 'edit_comment', $orphan_review ) ); + $this->assertTrue( user_can( $power_user->ID, 'delete_comment', $orphan_review ) ); + $this->assertTrue( user_can( $power_user->ID, 'moderate_comment', $orphan_review ) ); + + // The edit_posts fallback for orphaned default comments does not apply. + $this->assertFalse( user_can( self::$admin_id, 'edit_comment', $orphan_review ) ); + } + + /** + * An invalid comment ID maps to do_not_allow for all three meta capabilities. + * + * @ticket 35214 + */ + public function test_invalid_comment_id_is_denied_for_all_meta_caps(): void { + $invalid_id = PHP_INT_MAX; + + foreach ( array( 'edit_comment', 'delete_comment', 'moderate_comment' ) as $meta_cap ) { + $this->assertSame( array( 'do_not_allow' ), map_meta_cap( $meta_cap, self::$admin_id, $invalid_id ), "{$meta_cap} should map to do_not_allow." ); + $this->assertFalse( user_can( self::$admin_id, $meta_cap, $invalid_id ), "Admin should not have {$meta_cap} for an invalid comment." ); + } + + if ( is_multisite() ) { + grant_super_admin( self::$admin_id ); + $this->assertFalse( user_can( self::$admin_id, 'edit_comment', $invalid_id ), 'do_not_allow should deny even super admins.' ); + revoke_super_admin( self::$admin_id ); + } + } + + /** + * @ticket 35214 + * + * @expectedIncorrectUsage map_meta_cap + */ + public function test_delete_comment_without_comment_id_is_denied(): void { + $this->assertFalse( user_can( self::$admin_id, 'delete_comment' ) ); + } + + /** + * An anonymous comment (user_id 0) never matches the "own comment" branch + * of the independent model. + * + * @ticket 35214 + */ + public function test_anonymous_independent_comment_never_matches_own(): void { + $anon_review = $this->make_comment( 'review', 0 ); + + $author = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) ); + $author->add_cap( 'edit_reviews' ); + + $this->assertFalse( user_can( $author->ID, 'edit_comment', $anon_review ), 'edit_reviews alone should not grant editing an anonymous comment.' ); + + $editor = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) ); + $editor->add_cap( 'edit_others_reviews' ); + + $this->assertTrue( user_can( $editor->ID, 'edit_comment', $anon_review ), 'edit_others_reviews should grant editing an anonymous comment.' ); + } + + /** + * An array capability_type supplies the plural base enforced by map_meta_cap(). + * + * @ticket 35214 + */ + public function test_array_capability_type_is_enforced_end_to_end(): void { + register_comment_type( 'story_note', array( 'capability_type' => array( 'story', 'stories' ) ) ); + + $story_id = $this->make_comment( 'story_note', self::$admin_id ); + + $editor = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) ); + $editor->add_cap( 'edit_others_stories' ); + + $this->assertTrue( user_can( $editor->ID, 'edit_comment', $story_id ) ); + $this->assertFalse( user_can( self::$subscriber_id, 'edit_comment', $story_id ) ); + + $moderator = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) ); + $moderator->add_cap( 'moderate_stories' ); + + $this->assertTrue( user_can( $moderator->ID, 'moderate_comment', $story_id ) ); + $this->assertFalse( user_can( self::$admin_id, 'moderate_comment', $story_id ) ); + } + + /** + * Overriding a single meta capability flips only that action to the + * independent model; the other actions keep the default model. + * + * This also pins the documented footgun: with the default capability_type, + * the flipped action is gated by generated primitives no default role has. + * + * @ticket 35214 + */ + public function test_single_capability_override_flips_only_that_action(): void { + register_comment_type( + 'annotation', + array( + 'capabilities' => array( 'edit_comment' => 'edit_annotation' ), + ) + ); + + $annotation_id = $this->make_comment( 'annotation' ); + + // Editing is now independent, gated by 'edit_others_comments' - which even admins lack. + $this->assertFalse( user_can( self::$admin_id, 'edit_comment', $annotation_id ) ); + + // Deletion and moderation stay on the default model. + $this->assertTrue( user_can( self::$admin_id, 'delete_comment', $annotation_id ) ); + $this->assertTrue( user_can( self::$admin_id, 'moderate_comment', $annotation_id ) ); + } + + /** + * Unregistering a type reverts its stored comments to the default model + * (the plugin-deactivation scenario). + * + * @ticket 35214 + */ + public function test_unregistered_type_falls_back_to_default_model(): void { + $review_id = $this->make_comment( 'review' ); + + $moderator = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) ); + $moderator->add_cap( 'moderate_reviews' ); + + $this->assertTrue( user_can( $moderator->ID, 'moderate_comment', $review_id ) ); + $this->assertFalse( user_can( self::$admin_id, 'moderate_comment', $review_id ) ); + + unregister_comment_type( 'review' ); + + $this->assertFalse( user_can( $moderator->ID, 'moderate_comment', $review_id ) ); + $this->assertTrue( user_can( self::$admin_id, 'moderate_comment', $review_id ) ); + $this->assertTrue( user_can( self::$admin_id, 'edit_comment', $review_id ) ); + } + + /** + * The default model still consults a trashed parent post normally. + * + * @ticket 35214 + */ + public function test_default_model_still_consults_trashed_parent_post(): void { + $post_id = self::factory()->post->create( array( 'post_author' => self::$admin_id ) ); + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_type' => 'comment', + ) + ); + + wp_trash_post( $post_id ); + + $this->assertTrue( user_can( self::$admin_id, 'edit_comment', $comment_id ) ); + $this->assertFalse( user_can( self::$subscriber_id, 'edit_comment', $comment_id ) ); + } + + /* + * Custom meta capability names: current_user_can( 'edit_review', $comment_id ) + * translates to the generic meta capability, mirroring post type meta caps. + */ + + /** + * @ticket 35214 + */ + public function test_custom_meta_cap_name_translates_via_map_meta_cap(): void { + $review_id = $this->make_comment( 'review' ); + + $moderator = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) ); + $moderator->add_cap( 'moderate_reviews' ); + + $this->assertTrue( user_can( $moderator->ID, 'moderate_review', $review_id ) ); + $this->assertFalse( user_can( self::$subscriber_id, 'moderate_review', $review_id ) ); + + $this->assertSame( + map_meta_cap( 'moderate_comment', $moderator->ID, $review_id ), + map_meta_cap( 'moderate_review', $moderator->ID, $review_id ), + 'The custom name should map exactly like the generic name.' + ); + } + + /** + * @ticket 35214 + */ + public function test_custom_edit_meta_cap_name_respects_own_others_split(): void { + $author = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) ); + $author->add_cap( 'edit_reviews' ); + + $own_review = $this->make_comment( 'review', $author->ID ); + $others_review = $this->make_comment( 'review', self::$admin_id ); + + $this->assertTrue( user_can( $author->ID, 'edit_review', $own_review ) ); + $this->assertFalse( user_can( $author->ID, 'edit_review', $others_review ) ); + } + + /** + * Unregistering a type removes its custom meta capability translations. + * + * @ticket 35214 + */ + public function test_unregistering_type_removes_custom_meta_cap_translation(): void { + $review_id = $this->make_comment( 'review' ); + + $moderator = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) ); + $moderator->add_cap( 'moderate_reviews' ); + + $this->assertTrue( user_can( $moderator->ID, 'moderate_review', $review_id ) ); + + unregister_comment_type( 'review' ); + + $this->assertFalse( user_can( $moderator->ID, 'moderate_review', $review_id ), 'The translation should be removed with the type.' ); + } + + /** + * A comment type cannot claim a meta capability name already registered + * for a post type; the post type translation is preserved. + * + * @ticket 35214 + * + * @expectedIncorrectUsage register_comment_type + */ + public function test_comment_type_cannot_claim_post_type_meta_cap_names(): void { + global $post_type_meta_caps, $comment_type_meta_caps; + + register_post_type( + 'book', + array( + 'capability_type' => 'book', + 'map_meta_cap' => true, + ) + ); + register_comment_type( 'book_note', array( 'capability_type' => 'book' ) ); + + $this->assertSame( 'edit_post', ( (array) $post_type_meta_caps )['edit_book'] ?? null, 'The post type translation should be preserved.' ); + $this->assertArrayNotHasKey( 'edit_book', (array) $comment_type_meta_caps ); + $this->assertArrayNotHasKey( 'delete_book', (array) $comment_type_meta_caps ); + + // The non-colliding moderation capability is still registered. + $this->assertSame( 'moderate_comment', ( (array) $comment_type_meta_caps )['moderate_book'] ?? null ); + } + + /** + * A non-string capability name is ignored rather than stored as a translation. + * + * @ticket 35214 + * + * @covers ::get_comment_type_capabilities + */ + public function test_non_string_meta_cap_name_is_not_stored(): void { + global $comment_type_meta_caps; + + $before = (array) $comment_type_meta_caps; + + get_comment_type_capabilities( + (object) array( + 'capability_type' => 'comment', + 'capabilities' => array( 'edit_comment' => array( 'edit_odd' ) ), + ) + ); + + $this->assertSame( $before, (array) $comment_type_meta_caps ); + } +} diff --git a/tests/phpunit/tests/user/capabilities.php b/tests/phpunit/tests/user/capabilities.php index b41d7c2aaaeb6..601f4b4a9aefa 100644 --- a/tests/phpunit/tests/user/capabilities.php +++ b/tests/phpunit/tests/user/capabilities.php @@ -556,6 +556,8 @@ public function test_meta_caps_tests_are_correct() { $expected['delete_post_meta'], $expected['add_post_meta'], $expected['edit_comment'], + $expected['delete_comment'], + $expected['moderate_comment'], $expected['edit_comment_meta'], $expected['delete_comment_meta'], $expected['add_comment_meta'],