diff --git a/src/wp-includes/capabilities.php b/src/wp-includes/capabilities.php index 028e61ec414a8..3980dd4ded488 100644 --- a/src/wp-includes/capabilities.php +++ b/src/wp-includes/capabilities.php @@ -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. @@ -552,6 +556,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 +565,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.1.0' ); $caps[] = 'do_not_allow'; @@ -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': @@ -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', diff --git a/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php index 6bc4f6c606976..ccac539ec88d9 100644 --- a/src/wp-includes/class-wp-comment-type.php +++ b/src/wp-includes/class-wp-comment-type.php @@ -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 diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index 132e20e2585b6..d192e804fc86d 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -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.' ) ); @@ -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 ] ); /** @@ -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 @@ -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; + } } /** diff --git a/tests/phpunit/includes/abstract-testcase.php b/tests/phpunit/includes/abstract-testcase.php index 3cab3b3c042c5..fc00cc4d542b2 100644 --- a/tests/phpunit/includes/abstract-testcase.php +++ b/tests/phpunit/includes/abstract-testcase.php @@ -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 ); } diff --git a/tests/phpunit/tests/comment/commentCapabilities.php b/tests/phpunit/tests/comment/commentCapabilities.php new file mode 100644 index 0000000000000..9c8a77ff7da50 --- /dev/null +++ b/tests/phpunit/tests/comment/commentCapabilities.php @@ -0,0 +1,451 @@ +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() { + 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( $comment_type = 'comment', $user_id = 0 ) { + 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() { + $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() { + $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() { + $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() { + $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() { + $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() { + $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() { + $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() { + $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() { + $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() { + $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() { + $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() { + $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() { + $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() { + 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() { + 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() { + $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() { + $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() { + $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() { + $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() { + $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() { + 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', $post_type_meta_caps['edit_book'], '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', $comment_type_meta_caps['moderate_book'] ); + } +} diff --git a/tests/phpunit/tests/user/capabilities.php b/tests/phpunit/tests/user/capabilities.php index b92b0db231ecb..3aac321d2060a 100644 --- a/tests/phpunit/tests/user/capabilities.php +++ b/tests/phpunit/tests/user/capabilities.php @@ -552,6 +552,8 @@ public function testMetaCapsTestsAreCorrect() { $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'],