diff --git a/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php index 886fadaf14f3d..371d5730e0adb 100644 --- a/src/wp-includes/class-wp-comment-type.php +++ b/src/wp-includes/class-wp-comment-type.php @@ -85,6 +85,28 @@ final class WP_Comment_Type { */ public $internal = false; + /** + * The base used to build the edit, delete, and moderate capabilities. + * + * Registered as an array of singular and plural bases, this holds the + * singular base. Default 'comment'. + * + * @since 7.3.0 + * @var string + */ + public $capability_type = 'comment'; + + /** + * Capabilities for this comment type. + * + * Advisory only; map_meta_cap() does not consult them yet. + * See get_comment_type_capabilities(). + * + * @since 7.3.0 + * @var stdClass + */ + public $cap; + /** * Whether this comment type is a native or "built-in" comment type. * @@ -172,17 +194,27 @@ public function set_props( $args ): void { // No 'label' default, so get_comment_type_labels() can fall back to the default name. $defaults = array( - 'labels' => array(), - 'description' => '', - 'public' => true, - 'internal' => false, - '_builtin' => false, + 'labels' => array(), + 'description' => '', + 'public' => true, + 'internal' => false, + 'capability_type' => 'comment', + 'capabilities' => array(), + '_builtin' => false, ); $args = array_merge( $defaults, $args ); $args['name'] = $this->name; + $this->cap = get_comment_type_capabilities( (object) $args ); + unset( $args['capabilities'] ); + + // Collapse an array capability type back to its singular base. + if ( is_array( $args['capability_type'] ) ) { + $args['capability_type'] = $args['capability_type'][0]; + } + // The hierarchical label slots are null, so a true value would null every default label. $args['hierarchical'] = false; diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index 765514ca1f508..df2c1f6503002 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -434,23 +434,32 @@ function create_initial_comment_types(): void { * @param array|string $args { * Optional. Array or string of arguments for registering a comment type. Default empty array. * - * @type string $label Name of the comment type. Usually plural. - * Default is the value of $labels['name']. - * @type string[] $labels An array of labels for this comment type. If not set, the - * default comment labels are used. See get_comment_type_labels() - * for a full list of supported labels. - * @type string $description A short descriptive summary of what the comment type is. - * Default empty. - * @type bool $public Whether the comment type is intended for use publicly either via - * the admin interface or by front-end users. Core does not - * currently act on this argument. Default true. - * @type bool $internal Whether the comment type is for internal use only. Internal - * types are meant to be excluded from comment queries and counts - * by default. Core does not currently act on this argument. - * Default false. - * @type bool $_builtin For internal core use only. Marks the type as native to - * WordPress, which blocks it from being re-registered or - * unregistered. Default false. + * @type string $label Name of the comment type. Usually plural. + * Default is the value of $labels['name']. + * @type string[] $labels An array of labels for this comment type. If not set, the + * default comment labels are used. See + * get_comment_type_labels() for a full list of supported + * labels. + * @type string $description A short descriptive summary of what the comment type is. + * Default empty. + * @type bool $public Whether the comment type is intended for use publicly + * either via the admin interface or by front-end users. + * Core does not currently act on this argument. + * Default true. + * @type bool $internal Whether the comment type is for internal use only. + * Internal types are meant to be excluded from comment + * queries and counts by default. Core does not currently + * act on this argument. + * Default false. + * @type string|array $capability_type The base used to build the edit, delete, and moderate + * capabilities. May be an array of singular and plural + * bases, e.g. array( 'story', 'stories' ). Default 'comment'. + * @type string[] $capabilities Capabilities for this comment type, overriding those built + * from $capability_type. Advisory only; core does not + * enforce them. See get_comment_type_capabilities(). + * @type bool $_builtin For internal core use only. Marks the type as native to + * WordPress, which blocks it from being re-registered or + * unregistered. Default false. * } * @return WP_Comment_Type|WP_Error The registered comment type object on success, * WP_Error object on failure. @@ -641,7 +650,7 @@ function comment_type_exists( $comment_type ) { * @since 7.3.0 * * @param WP_Comment_Type $comment_type_object Comment type object. - * @return stdClass { + * @return object { * Comment type labels object. * * @type string $name General name for the comment type, usually plural. The same as and @@ -698,6 +707,70 @@ function get_comment_type_labels( $comment_type_object ) { return $labels; } +/** + * Builds an object with all comment type capabilities out of a comment type object. + * + * Capabilities not set in the `capabilities` argument are built from the + * `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. + * + * 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. + * + * Normalizes the `capability_type` property of `$args` to its array form. + * + * @since 7.3.0 + * + * @param object $args Comment type registration arguments. Expects the + * `capability_type` and `capabilities` properties. + * @return stdClass { + * Object with all the capabilities as member variables. + * + * @type string $edit_comment Meta capability to edit a comment of this type. + * Default 'edit_comment'. + * @type string $delete_comment Meta capability to delete a comment of this type. + * Default 'delete_comment'. + * @type string $moderate_comment Meta capability to moderate a comment of this type. + * Default 'moderate_comment'. + * @type string $edit_comments Primitive capability to edit comments of this type. + * Default 'edit_comments'. + * @type string $edit_others_comments Primitive capability to edit comments of this type + * authored by other users. Default 'edit_others_comments'. + * @type string $delete_comments Primitive capability to delete comments of this type. + * Default 'delete_comments'. + * @type string $moderate_comments Primitive capability to moderate comments of this type. + * Default 'moderate_comments'. + * } + */ +function get_comment_type_capabilities( object $args ): stdClass { + if ( ! is_array( $args->capability_type ) ) { + $args->capability_type = array( $args->capability_type, $args->capability_type . 's' ); + } + + // Singular base for meta capabilities, plural base for primitive capabilities. + list( $singular_base, $plural_base ) = $args->capability_type; + + $default_capabilities = array( + // Meta capabilities. + 'edit_comment' => 'edit_' . $singular_base, + 'delete_comment' => 'delete_' . $singular_base, + 'moderate_comment' => 'moderate_' . $singular_base, + // Primitive capabilities. + 'edit_comments' => 'edit_' . $plural_base, + 'edit_others_comments' => 'edit_others_' . $plural_base, + 'delete_comments' => 'delete_' . $plural_base, + 'moderate_comments' => 'moderate_' . $plural_base, + ); + + $capabilities = array_merge( $default_capabilities, $args->capabilities ); + + return (object) $capabilities; +} + /** * Retrieves all of the WordPress supported comment statuses. * @@ -1887,7 +1960,7 @@ function wp_check_comment_disallowed_list( $author, $email, $url, $comment, $use * * @param int $post_id Optional. Restrict the comment counts to the given post. Default 0, which indicates that * comment counts for the whole site will be retrieved. - * @return stdClass { + * @return object { * The number of comments keyed by their status. * * @type int $approved The number of approved comments. diff --git a/tests/phpunit/tests/comment/types.php b/tests/phpunit/tests/comment/types.php index e1702e5e5c720..bf80fa6e5694a 100644 --- a/tests/phpunit/tests/comment/types.php +++ b/tests/phpunit/tests/comment/types.php @@ -574,6 +574,186 @@ public function test_registered_comment_type_action_receives_type_and_object(): $this->assertSame( 'foo', $args[0][1]->name ); } + /** + * @ticket 35214 + * + * @covers ::register_comment_type + */ + public function test_registered_comment_type_exposes_cap_object(): void { + register_comment_type( 'foo', array( 'capability_type' => 'review' ) ); + + $cobj = get_comment_type_object( 'foo' ); + + $this->assertSame( 'edit_reviews', $cobj->cap->edit_comments ); + $this->assertSame( 'moderate_reviews', $cobj->cap->moderate_comments ); + } + + /** + * The built-in comment type's capabilities match the existing core comment capabilities. + * + * @ticket 35214 + * + * @covers ::get_comment_type_capabilities + */ + public function test_built_in_comment_type_capabilities_are_backward_compatible(): void { + $cobj = get_comment_type_object( 'comment' ); + + $this->assertSame( 'edit_comment', $cobj->cap->edit_comment ); + $this->assertSame( 'moderate_comments', $cobj->cap->moderate_comments ); + } + + /** + * @ticket 35214 + * + * @covers ::get_comment_type_capabilities + */ + public function test_get_comment_type_capabilities_from_string(): void { + $caps = get_comment_type_capabilities( + (object) array( + 'capability_type' => 'review', + 'capabilities' => array(), + ) + ); + + $this->assertSame( 'edit_review', $caps->edit_comment ); + $this->assertSame( 'edit_reviews', $caps->edit_comments ); + $this->assertSame( 'edit_others_reviews', $caps->edit_others_comments ); + $this->assertSame( 'delete_review', $caps->delete_comment ); + $this->assertSame( 'moderate_reviews', $caps->moderate_comments ); + } + + /** + * @ticket 35214 + * + * @covers ::get_comment_type_capabilities + */ + public function test_get_comment_type_capabilities_honors_capabilities_override(): void { + $caps = get_comment_type_capabilities( + (object) array( + 'capability_type' => 'comment', + 'capabilities' => array( + 'edit_comments' => 'manage_stuff', + ), + ) + ); + + $this->assertSame( 'manage_stuff', $caps->edit_comments ); + } + + /** + * The full set of meta and primitive capabilities is generated from the base. + * + * @ticket 35214 + * + * @covers ::get_comment_type_capabilities + */ + public function test_get_comment_type_capabilities_generates_full_set(): void { + $caps = get_comment_type_capabilities( + (object) array( + 'capability_type' => 'review', + 'capabilities' => array(), + ) + ); + + $expected = array( + // Meta capabilities. + 'edit_comment' => 'edit_review', + 'delete_comment' => 'delete_review', + 'moderate_comment' => 'moderate_review', + // Primitive capabilities. + 'edit_comments' => 'edit_reviews', + 'edit_others_comments' => 'edit_others_reviews', + 'delete_comments' => 'delete_reviews', + 'moderate_comments' => 'moderate_reviews', + ); + + $this->assertSame( $expected, (array) $caps ); + } + + /** + * An array capability type supplies an explicit plural base for primitive caps. + * + * @ticket 35214 + * + * @covers ::get_comment_type_capabilities + */ + public function test_get_comment_type_capabilities_from_array(): void { + $caps = get_comment_type_capabilities( + (object) array( + 'capability_type' => array( 'story', 'stories' ), + 'capabilities' => array(), + ) + ); + + // Singular base drives the meta capabilities. + $this->assertSame( 'edit_story', $caps->edit_comment ); + $this->assertSame( 'delete_story', $caps->delete_comment ); + // Explicit plural base drives the primitive capabilities. + $this->assertSame( 'edit_stories', $caps->edit_comments ); + $this->assertSame( 'delete_stories', $caps->delete_comments ); + $this->assertSame( 'moderate_stories', $caps->moderate_comments ); + } + + /** + * No 'read_comment' meta capability is generated, as core defines no semantics + * for reading a single comment. + * + * @ticket 35214 + * + * @covers ::get_comment_type_capabilities + */ + public function test_get_comment_type_capabilities_omits_read_comment(): void { + $caps = get_comment_type_capabilities( + (object) array( + 'capability_type' => 'review', + 'capabilities' => array(), + ) + ); + + $this->assertObjectNotHasProperty( 'read_comment', $caps ); + $this->assertObjectNotHasProperty( 'read_comment', get_comment_type_object( 'comment' )->cap ); + } + + /** + * The built-in types register without a capability type, so they share one cap set. + * + * @ticket 35214 + * + * @covers ::get_comment_type_capabilities + * + * @dataProvider data_built_in_comment_types + * + * @param string $comment_type Built-in comment type name. + */ + public function test_built_in_comment_types_share_the_comment_capabilities( string $comment_type ): void { + $this->assertEquals( + get_comment_type_object( 'comment' )->cap, + get_comment_type_object( $comment_type )->cap + ); + } + + /** + * An override applies to meta capabilities as well as primitives, so a type can point a + * meta capability at a name that map_meta_cap() already resolves. + * + * @ticket 35214 + * + * @covers ::get_comment_type_capabilities + */ + public function test_get_comment_type_capabilities_honors_a_meta_capability_override(): void { + $caps = get_comment_type_capabilities( + (object) array( + 'capability_type' => 'review', + 'capabilities' => array( + 'edit_comment' => 'edit_review_item', + ), + ) + ); + + $this->assertSame( 'edit_review_item', $caps->edit_comment, 'The override should win.' ); + $this->assertSame( 'delete_review', $caps->delete_comment, 'The rest should still derive from the base.' ); + } + /** * Comment types are never hierarchical. The default labels reserve the hierarchical * slot as null, so honoring a provided value would resolve every label to null. diff --git a/tests/phpunit/tests/comment/wpCommentType.php b/tests/phpunit/tests/comment/wpCommentType.php index bae6b5100d248..74ab3349683d2 100644 --- a/tests/phpunit/tests/comment/wpCommentType.php +++ b/tests/phpunit/tests/comment/wpCommentType.php @@ -121,4 +121,81 @@ public function test_reset_default_labels_clears_cache(): void { $labels = WP_Comment_Type::get_default_labels(); $this->assertSame( 'Comments', $labels['name'][0], 'Resetting should rebuild the default labels.' ); } + + /** + * @ticket 35214 + * + * @covers ::set_props + */ + public function test_default_capability_type_and_cap_object(): void { + $comment_type = new WP_Comment_Type( 'foo' ); + + $this->assertSame( 'comment', $comment_type->capability_type ); + $this->assertIsObject( $comment_type->cap ); + $this->assertSame( 'edit_comment', $comment_type->cap->edit_comment ); + $this->assertSame( 'moderate_comments', $comment_type->cap->moderate_comments ); + } + + /** + * @ticket 35214 + * + * @covers ::set_props + */ + public function test_custom_capability_type_builds_cap_object(): void { + $comment_type = new WP_Comment_Type( 'foo', array( 'capability_type' => 'review' ) ); + + $this->assertSame( 'review', $comment_type->capability_type ); + $this->assertSame( 'edit_review', $comment_type->cap->edit_comment ); + $this->assertSame( 'edit_reviews', $comment_type->cap->edit_comments ); + $this->assertSame( 'moderate_reviews', $comment_type->cap->moderate_comments ); + } + + /** + * An array capability type allows an explicit plural and is collapsed to its singular base. + * + * @ticket 35214 + * + * @covers ::set_props + */ + public function test_array_capability_type_uses_explicit_plural(): void { + $comment_type = new WP_Comment_Type( 'foo', array( 'capability_type' => array( 'story', 'stories' ) ) ); + + $this->assertSame( 'story', $comment_type->capability_type ); + $this->assertSame( 'edit_story', $comment_type->cap->edit_comment ); + $this->assertSame( 'edit_stories', $comment_type->cap->edit_comments ); + } + + /** + * @ticket 35214 + * + * @covers ::set_props + */ + public function test_capabilities_argument_overrides_generated_caps(): void { + $comment_type = new WP_Comment_Type( + 'foo', + array( + 'capability_type' => 'review', + 'capabilities' => array( + 'moderate_comments' => 'manage_reviews', + ), + ) + ); + + $this->assertSame( 'manage_reviews', $comment_type->cap->moderate_comments ); + // Non-overridden caps are still generated from the capability type. + $this->assertSame( 'edit_reviews', $comment_type->cap->edit_comments ); + } + + /** + * The input `capabilities` array is consumed and not kept as a public property. + * + * @ticket 35214 + * + * @covers ::set_props + */ + public function test_capabilities_input_is_not_retained_as_property(): void { + $comment_type = new WP_Comment_Type( 'foo', array( 'capabilities' => array( 'edit_comments' => 'x' ) ) ); + + $this->assertObjectNotHasProperty( 'capabilities', $comment_type ); + } }