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
1 change: 0 additions & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ includes:
- tests/phpstan/baselines/empty.offset.neon
- tests/phpstan/baselines/empty.property.neon
- tests/phpstan/baselines/empty.variable.neon
- tests/phpstan/baselines/foreach.nonIterable.neon
- tests/phpstan/baselines/function.alreadyNarrowedType.neon
- tests/phpstan/baselines/function.impossibleType.neon
- tests/phpstan/baselines/function.resultUnused.neon
Expand Down
14 changes: 5 additions & 9 deletions src/wp-includes/class-wp-post-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -797,13 +797,14 @@ public function remove_supports() {
* Removes any rewrite rules, permastructs, and rules for the post type.
*
* @since 4.6.0
* @since 7.2.0 Registered meta capabilities are no longer removed here. They are rebuilt
* from the post types that remain by {@see unregister_post_type()}.
*
* @global WP_Rewrite $wp_rewrite WordPress rewrite component.
* @global WP $wp Current WordPress environment instance.
* @global array $post_type_meta_caps Used to remove meta capabilities.
* @global WP_Rewrite $wp_rewrite WordPress rewrite component.
* @global WP $wp Current WordPress environment instance.
*/
public function remove_rewrite_rules() {
global $wp, $wp_rewrite, $post_type_meta_caps;
global $wp, $wp_rewrite;

// Remove query var.
if ( false !== $this->query_var ) {
Expand All @@ -820,11 +821,6 @@ public function remove_rewrite_rules() {
}
}
}

// Remove registered custom meta capabilities.
foreach ( $this->cap as $cap ) {
unset( $post_type_meta_caps[ $cap ] );
}
}

/**
Expand Down
36 changes: 27 additions & 9 deletions src/wp-includes/post.php
Original file line number Diff line number Diff line change
Expand Up @@ -1917,20 +1917,20 @@ function register_post_type( $post_type, $args = array() ) {
*
* @since 4.5.0
*
* @global array $wp_post_types List of post types.
* @global array<string, string> $post_type_meta_caps Used to store meta capabilities.
* @global array<string, WP_Post_Type> $wp_post_types List of post types.
*
* @param string $post_type Post type to unregister.
* @return true|WP_Error True on success, WP_Error on failure or if the post type doesn't exist.
*/
function unregister_post_type( $post_type ) {
global $wp_post_types;
global $post_type_meta_caps, $wp_post_types;

if ( ! post_type_exists( $post_type ) ) {
$post_type_object = get_post_type_object( $post_type );
if ( ! $post_type_object ) {
return new WP_Error( 'invalid_post_type', __( 'Invalid post type.' ) );
}

$post_type_object = get_post_type_object( $post_type );

// Do not allow unregistering internal post types.
if ( $post_type_object->_builtin ) {
return new WP_Error( 'invalid_post_type', __( 'Unregistering a built-in post type is not allowed' ) );
Expand All @@ -1944,6 +1944,20 @@ function unregister_post_type( $post_type ) {

unset( $wp_post_types[ $post_type ] );

/*
* Rebuild the meta capabilities of the post types that remain.
*
* They are keyed by the custom capability name, so a single entry may be owed to any
* number of registered post types. Removing the entries for this post type alone could
* therefore remove entries that the others still depend on.
*/
$post_type_meta_caps = array();
foreach ( $wp_post_types as $registered_post_type ) {
if ( $registered_post_type->map_meta_cap ) {
_post_type_meta_capabilities( get_object_vars( $registered_post_type->cap ) );
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are the implications here for registering a post type? does this go both-ways, where something gets potentially overwritten that shouldn’t be?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at how this works…

When register_post_type() is called during init, it constructs a WP_Post_Type. The constructor will call WP_Post_Type::set_props() which in turn calls get_post_type_capabilities(). This get_post_type_capabilities() function will also call _post_type_meta_capabilities() if map_meta_cap is true, which results in $post_type_meta_caps being populated. So the end result with just register_post_type() being called multiple times is the $post_type_meta_caps being rebuilt from scratch in the same way as this is being done here when one of the post types is unregistered.


/**
* Fires after a post type was unregistered.
*
Expand Down Expand Up @@ -2081,16 +2095,20 @@ function get_post_type_capabilities( $args ) {
}

/**
* Stores or returns a list of post type meta caps for map_meta_cap().
* Stores a list of post type meta caps for {@see map_meta_cap()}.
*
* @since 3.1.0
* @since 4.5.0 The list moved to the `$post_type_meta_caps` global and the function
* no longer returns it when called without arguments.
* @since 7.2.0 The `$capabilities` parameter defaults to an empty array rather than `null`.
* @access private
*
* @global array $post_type_meta_caps Used to store meta capabilities.
* @global array<string, string> $post_type_meta_caps Used to store meta capabilities.
*
* @param string[] $capabilities Post type meta capabilities.
* @param array<string, string> $capabilities Map of core meta capability name to the custom
* capability name it is registered under.
*/
function _post_type_meta_capabilities( $capabilities = null ) {
function _post_type_meta_capabilities( $capabilities = array() ): void {
global $post_type_meta_caps;

foreach ( $capabilities as $core => $custom ) {
Expand Down
25 changes: 0 additions & 25 deletions tests/phpstan/baselines/foreach.nonIterable.neon

This file was deleted.

154 changes: 154 additions & 0 deletions tests/phpunit/tests/post/types.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ class Tests_Post_Types extends WP_UnitTestCase {
*/
public $post_type;

/**
* Author user ID.
*/
public static int $author_id;

/**
* Sets up shared fixtures.
*
* @param WP_UnitTest_Factory $factory Factory instance.
*/
public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
self::$author_id = $factory->user->create( array( 'role' => 'author' ) );
}

/**
* Set up.
*
Expand Down Expand Up @@ -419,6 +433,146 @@ public function test_unregister_post_type_removes_custom_meta_capabilities() {
$this->assertArrayNotHasKey( 'edit_bar', $post_type_meta_caps );
}

/**
* Tests that meta capabilities shared with another registered post type are retained.
*
* Meta capabilities are stored keyed by the custom capability name, so post types
* sharing a capability type resolve to the same entries. Unregistering one of them
* must not remove the entries the others still rely on.
*
* @ticket 66008
*
* @global array<string, string> $post_type_meta_caps Used to store meta capabilities.
*/
public function test_unregister_post_type_retains_meta_capabilities_shared_with_another_post_type() {
global $post_type_meta_caps;

$args = array(
'public' => true,
'capability_type' => 'publication',
'map_meta_cap' => true,
);

register_post_type( 'book', $args );
register_post_type( 'magazine', $args );

$this->assertSame( 'read_post', $post_type_meta_caps['read_publication'], 'The read meta capability was not registered.' );
$this->assertSame( 'delete_post', $post_type_meta_caps['delete_publication'], 'The delete meta capability was not registered.' );
$this->assertSame( 'edit_post', $post_type_meta_caps['edit_publication'], 'The edit meta capability was not registered.' );

$this->assertTrue( unregister_post_type( 'book' ) );

$this->assertSame( 'read_post', $post_type_meta_caps['read_publication'], 'The read meta capability of the remaining post type was removed.' );
$this->assertSame( 'delete_post', $post_type_meta_caps['delete_publication'], 'The delete meta capability of the remaining post type was removed.' );
$this->assertSame( 'edit_post', $post_type_meta_caps['edit_publication'], 'The edit meta capability of the remaining post type was removed.' );
}

/**
* Tests that a remaining post type's meta capabilities still map down to primitive capabilities.
*
* @ticket 66008
*/
public function test_unregister_post_type_retains_meta_capability_mapping_for_another_post_type() {
$args = array(
'public' => true,
'capability_type' => 'publication',
'map_meta_cap' => true,
);

register_post_type( 'book', $args );
register_post_type( 'magazine', $args );

$post_id = self::factory()->post->create(
array(
'post_type' => 'magazine',
'post_status' => 'publish',
'post_author' => self::$author_id,
)
);

$this->assertSame(
array( 'edit_published_publications' ),
map_meta_cap( 'edit_publication', self::$author_id, $post_id ),
'The meta capability did not map to a primitive capability.'
);

$this->assertTrue( unregister_post_type( 'book' ) );

$this->assertSame(
array( 'edit_published_publications' ),
map_meta_cap( 'edit_publication', self::$author_id, $post_id ),
'The meta capability of the remaining post type no longer maps to a primitive capability.'
);
}

/**
* Tests that a post type which does not map meta capabilities removes none on unregistration.
*
* Such a post type never stores any meta capabilities, so it must not remove the
* identically named entries belonging to the built-in post types.
*
* @ticket 66008
*
* @global array<string, string> $post_type_meta_caps Used to store meta capabilities.
*/
public function test_unregister_post_type_retains_meta_capabilities_when_not_mapping_meta_caps() {
global $post_type_meta_caps;

register_post_type(
'foo',
array(
'public' => true,
'map_meta_cap' => false,
)
);

$this->assertTrue( unregister_post_type( 'foo' ) );

$this->assertSame( 'read_post', $post_type_meta_caps['read_post'], 'The built-in read meta capability was removed.' );
$this->assertSame( 'delete_post', $post_type_meta_caps['delete_post'], 'The built-in delete meta capability was removed.' );
$this->assertSame( 'edit_post', $post_type_meta_caps['edit_post'], 'The built-in edit meta capability was removed.' );
}

/**
* Tests that primitive capabilities are not treated as meta capabilities on unregistration.
*
* Only the read, delete and edit capabilities are stored as meta capabilities. A post type
* using one of those names as a primitive capability must not remove another post type's
* meta capability of the same name.
*
* @ticket 66008
*
* @global array<string, string> $post_type_meta_caps Used to store meta capabilities.
*/
public function test_unregister_post_type_retains_meta_capabilities_matching_primitive_capabilities() {
global $post_type_meta_caps;

register_post_type(
'book',
array(
'public' => true,
'capability_type' => 'book',
'map_meta_cap' => true,
)
);

// For this post type 'edit_book' is a primitive capability, not a meta capability.
register_post_type(
'shelf',
array(
'public' => true,
'map_meta_cap' => false,
'capabilities' => array(
'edit_posts' => 'edit_book',
),
)
);

$this->assertTrue( unregister_post_type( 'shelf' ) );

$this->assertSame( 'edit_post', $post_type_meta_caps['edit_book'], 'The meta capability of another post type was removed.' );
}

/**
* @ticket 14761
*/
Expand Down
Loading