diff --git a/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php index de6eff84b7549..6bc4f6c606976 100644 --- a/src/wp-includes/class-wp-comment-type.php +++ b/src/wp-includes/class-wp-comment-type.php @@ -90,6 +90,33 @@ final class WP_Comment_Type { */ public $internal = false; + /** + * The string to use to build the read, edit, and delete capabilities. + * + * May be registered as an array to allow for alternative plurals when using + * this argument as a base to construct the capabilities, e.g. + * array( 'story', 'stories' ). set_props() collapses the array form back to + * the singular base once the capabilities are built. Default 'comment'. + * + * @since 7.1.0 + * @var string + */ + public $capability_type = 'comment'; + + /** + * 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. + * + * @since 7.1.0 + * @var stdClass + */ + public $cap; + /** * Whether this comment type is a native or "built-in" comment type. * @@ -183,17 +210,28 @@ public function set_props( $args ) { * treated as a provided value and overwrite the default name with false. */ $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; + // Build the capabilities object, then remove the input array from the props. + $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]; + } + foreach ( $args as $property_name => $property_value ) { $this->$property_name = $property_value; } diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index b01a5c3c4bd8e..132e20e2585b6 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -361,19 +361,31 @@ function create_initial_comment_types() { * @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. Core does not - * currently consult this flag; it is intended to drive default - * query exclusions in the future. 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. Core + * does not currently consult this flag; it is intended to + * drive default query exclusions in the future. + * Default false. + * @type string|array $capability_type The string to use to build the read, edit, and delete + * capabilities. May be passed as an array to allow for + * alternative plurals when using this argument as a base to + * construct the capabilities, e.g. array( 'story', 'stories' ). + * Default 'comment'. + * @type string[] $capabilities Array of capabilities for this comment type. + * $capability_type is used as a base to construct + * capabilities by default. + * See get_comment_type_capabilities(). * } * @return WP_Comment_Type|WP_Error The registered comment type object on success, * WP_Error object on failure. @@ -611,6 +623,84 @@ function get_comment_type_labels( $comment_type_object ) { return $labels; } +/** + * Builds an object with all comment type capabilities out of a 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. + * + * 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' ). + * + * 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 + * default capability mapping; `moderate_comments` is the only generated + * primitive that default roles grant. Consumers should check the meta + * capabilities together with a comment ID instead of testing those primitives + * directly. + * + * Note: The `capability_type` property of the passed object is normalized to + * its array form as a side effect of calling this function, matching + * get_post_type_capabilities(). + * + * @since 7.1.0 + * + * @param object $args Comment type registration arguments. Expects the + * `capability_type` and `capabilities` properties. + * @return object { + * 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 $read_comment Meta capability to read a comment of this type. + * Default 'read_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( $args ) { + 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, + 'read_comment' => 'read_' . $singular_base, + 'delete_comment' => 'delete_' . $singular_base, + 'moderate_comment' => 'moderate_' . $singular_base, + // Primitive capabilities used outside of map_meta_cap(). + '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. * diff --git a/tests/phpunit/tests/comment/types.php b/tests/phpunit/tests/comment/types.php index bd824984fa7bb..fbe9d919a4074 100644 --- a/tests/phpunit/tests/comment/types.php +++ b/tests/phpunit/tests/comment/types.php @@ -522,4 +522,121 @@ public function test_registered_comment_type_action_receives_type_and_object() { $this->assertInstanceOf( 'WP_Comment_Type', $args[0][1] ); $this->assertSame( 'foo', $args[0][1]->name ); } + + /** + * @ticket 35214 + */ + public function test_registered_comment_type_exposes_cap_object() { + 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 + */ + public function test_built_in_comment_type_capabilities_are_backward_compatible() { + $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() { + $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() { + $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() { + $caps = get_comment_type_capabilities( + (object) array( + 'capability_type' => 'review', + 'capabilities' => array(), + ) + ); + + $expected = array( + // Meta capabilities. + 'edit_comment' => 'edit_review', + 'read_comment' => 'read_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() { + $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( 'read_story', $caps->read_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 ); + } } diff --git a/tests/phpunit/tests/comment/wpCommentType.php b/tests/phpunit/tests/comment/wpCommentType.php index dc316aac4a735..4e995137f02ab 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() { $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() { + $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() { + $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() { + $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() { + $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() { + $comment_type = new WP_Comment_Type( 'foo', array( 'capabilities' => array( 'edit_comments' => 'x' ) ) ); + + $this->assertObjectNotHasProperty( 'capabilities', $comment_type ); + } }