From 56de711f0a3061d271c315b1d71e11354b453d66 Mon Sep 17 00:00:00 2001 From: Igor Radovanov Date: Sun, 12 Oct 2025 20:59:32 +0200 Subject: [PATCH 1/3] Integrate sticky posts with Post Type Support API Makes stickiness a declared post type feature checked via post_type_supports() instead of inconsistent hardcoded checks. Updates core sticky functions and UI layers (get_inline_data, post_submit_meta_box, XML-RPC) to use unified post type support checks. Enables custom post types to declare sticky support while maintaining backward compatibility. Fixes #48954 --- src/wp-admin/includes/meta-boxes.php | 4 +- src/wp-admin/includes/template.php | 2 +- src/wp-includes/class-wp-xmlrpc-server.php | 4 +- src/wp-includes/post.php | 26 +++- tests/phpunit/tests/post/sticky.php | 165 +++++++++++++++++++++ 5 files changed, 194 insertions(+), 7 deletions(-) create mode 100644 tests/phpunit/tests/post/sticky.php diff --git a/src/wp-admin/includes/meta-boxes.php b/src/wp-admin/includes/meta-boxes.php index c52b4787a3044..344d673ccc129 100644 --- a/src/wp-admin/includes/meta-boxes.php +++ b/src/wp-admin/includes/meta-boxes.php @@ -181,7 +181,7 @@ function post_submit_meta_box( $post, $args = array() ) { } elseif ( ! empty( $post->post_password ) ) { $visibility = 'password'; $visibility_trans = __( 'Password protected' ); - } elseif ( 'post' === $post_type && is_sticky( $post_id ) ) { + } elseif ( post_type_supports( $post_type, 'sticky' ) && is_sticky( $post_id ) ) { $visibility = 'public'; $visibility_trans = __( 'Public, Sticky' ); } else { @@ -210,7 +210,7 @@ function post_submit_meta_box( $post, $args = array() ) { />
- + />
diff --git a/src/wp-admin/includes/template.php b/src/wp-admin/includes/template.php index 25fb44ad71890..020a13ce41941 100644 --- a/src/wp-admin/includes/template.php +++ b/src/wp-admin/includes/template.php @@ -376,7 +376,7 @@ function get_inline_data( $post ) { } } - if ( ! $post_type_object->hierarchical ) { + if ( post_type_supports( $post->post_type, 'sticky' ) ) { echo '
' . ( is_sticky( $post->ID ) ? 'sticky' : '' ) . '
'; } diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index 7c1e101e1e091..94ab51137c683 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -939,7 +939,7 @@ protected function _prepare_post( $post, $fields ) { 'menu_order' => (int) $post['menu_order'], 'comment_status' => $post['comment_status'], 'ping_status' => $post['ping_status'], - 'sticky' => ( 'post' === $post['post_type'] && is_sticky( $post['ID'] ) ), + 'sticky' => ( post_type_supports( $post['post_type'], 'sticky' ) && is_sticky( $post['ID'] ) ), ); // Thumbnail. @@ -6363,7 +6363,7 @@ public function mw_getRecentPosts( $args ) { 'wp_post_format' => $post_format, 'date_modified' => $post_modified, 'date_modified_gmt' => $post_modified_gmt, - 'sticky' => ( 'post' === $entry['post_type'] && is_sticky( $entry['ID'] ) ), + 'sticky' => ( post_type_supports( $entry['post_type'], 'sticky' ) && is_sticky( $entry['ID'] ) ), 'wp_post_thumbnail' => get_post_thumbnail_id( $entry['ID'] ), ); } diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index a2d8fba9db341..372090eddf8bb 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -43,6 +43,7 @@ function create_initial_post_types() { 'rest_controller_class' => 'WP_REST_Posts_Controller', ) ); + add_post_type_support( 'post', 'sticky' ); register_post_type( 'page', @@ -2858,6 +2859,13 @@ function is_sticky( $post_id = 0 ) { $post_id = get_the_ID(); } + $post = get_post( $post_id ); + + // Check if the post type supports stickiness. + if ( ! post_type_supports( $post->post_type, 'sticky' ) ) { + return false; + } + $stickies = get_option( 'sticky_posts' ); if ( is_array( $stickies ) ) { @@ -3263,7 +3271,15 @@ function sanitize_post_field( $field, $value, $post_id, $context = 'display' ) { * @param int $post_id Post ID. */ function stick_post( $post_id ) { - $post_id = (int) $post_id; + $post_id = (int) $post_id; + + $post = get_post( $post_id ); + + // Check if the post type supports stickiness. + if ( ! post_type_supports( $post->post_type, 'sticky' ) ) { + return; + } + $stickies = get_option( 'sticky_posts' ); $updated = false; @@ -3300,7 +3316,13 @@ function stick_post( $post_id ) { * @param int $post_id Post ID. */ function unstick_post( $post_id ) { - $post_id = (int) $post_id; + $post_id = (int) $post_id; + $post = get_post( $post_id ); + + // Check if the post type supports stickiness. + if ( ! post_type_supports( $post->post_type, 'sticky' ) ) { + return; + } $stickies = get_option( 'sticky_posts' ); if ( ! is_array( $stickies ) ) { diff --git a/tests/phpunit/tests/post/sticky.php b/tests/phpunit/tests/post/sticky.php new file mode 100644 index 0000000000000..eef3fc0dcb181 --- /dev/null +++ b/tests/phpunit/tests/post/sticky.php @@ -0,0 +1,165 @@ +post->create( array( 'post_type' => 'post' ) ); + + stick_post( $post_id ); + $this->assertTrue( is_sticky( $post_id ) ); + + unstick_post( $post_id ); + $this->assertFalse( is_sticky( $post_id ) ); + } + + /** + * Test that stickiness does not work for post types that do not support it. + */ + public function test_stickiness_for_unsupported_post_type() { + register_post_type( + 'custom_type', + array( + 'label' => 'Custom Type', + 'public' => true, + 'supports' => array( 'title', 'editor' ), + ) + ); + + $post_id = self::factory()->post->create( array( 'post_type' => 'custom_type' ) ); + stick_post( $post_id ); + $this->assertFalse( is_sticky( $post_id ) ); + } + + /** + * Test that stickiness is removed when switching to a post type that does not support it. + */ + public function test_stickiness_removed_on_post_type_switch() { + $post_id = self::factory()->post->create( array( 'post_type' => 'post' ) ); + stick_post( $post_id ); + $this->assertTrue( is_sticky( $post_id ) ); + + wp_update_post( + array( + 'ID' => $post_id, + 'post_type' => 'page', + ) + ); + $this->assertFalse( is_sticky( $post_id ) ); + } + + /** + * Test that UI functions handle post type switches correctly. + */ + public function test_ui_functions_after_post_type_switch() { + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + $post_id = self::factory()->post->create( + array( + 'post_type' => 'post', + 'post_author' => $admin_id, + ) + ); + stick_post( $post_id ); + + $states = get_post_states( get_post( $post_id ) ); + $this->assertArrayHasKey( 'sticky', $states ); + + wp_update_post( + array( + 'ID' => $post_id, + 'post_type' => 'page', + ) + ); + + $states = get_post_states( get_post( $post_id ) ); + $this->assertArrayNotHasKey( 'sticky', $states ); + } + + /** + * Test backward compatibility - existing sticky posts behavior. + */ + public function test_backward_compatibility() { + $this->assertTrue( post_type_supports( 'post', 'sticky' ) ); + $this->assertFalse( post_type_supports( 'page', 'sticky' ) ); + + $post_id = self::factory()->post->create( array( 'post_type' => 'post' ) ); + stick_post( $post_id ); + $this->assertTrue( is_sticky( $post_id ) ); + } + + /** + * Test that developers can add sticky support to custom post types. + */ + public function test_custom_post_type_sticky_support() { + register_post_type( + 'my_custom_type', + array( + 'label' => 'My Custom Type', + 'public' => true, + 'supports' => array( 'title', 'editor', 'sticky' ), + ) + ); + + $this->assertTrue( post_type_supports( 'my_custom_type', 'sticky' ) ); + + $custom_post_id = self::factory()->post->create( array( 'post_type' => 'my_custom_type' ) ); + stick_post( $custom_post_id ); + $this->assertTrue( is_sticky( $custom_post_id ) ); + + register_post_type( + 'another_type', + array( + 'label' => 'Another Type', + 'public' => true, + 'supports' => array( 'title' ), + ) + ); + + $this->assertFalse( post_type_supports( 'another_type', 'sticky' ) ); + + add_post_type_support( 'another_type', 'sticky' ); + $this->assertTrue( post_type_supports( 'another_type', 'sticky' ) ); + } + + /** + * Test that post_type_supports() checks work correctly for stickiness. + */ + public function test_post_type_supports_sticky() { + $this->assertTrue( post_type_supports( 'post', 'sticky' ) ); + $this->assertFalse( post_type_supports( 'page', 'sticky' ) ); + $this->assertFalse( post_type_supports( 'attachment', 'sticky' ) ); + } +} From 5acb38bf62afb6559ae30da8e5d8e0a4d953fd0d Mon Sep 17 00:00:00 2001 From: Igor Radovanov Date: Sun, 12 Oct 2025 21:00:14 +0200 Subject: [PATCH 2/3] Merge --- src/wp-includes/class-wp-xmlrpc-server.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index 94ab51137c683..c8eb9dcb31aa5 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -1466,10 +1466,8 @@ protected function _insert_post( $user, $content_struct ) { if ( get_post_type( $post_data['ID'] ) !== $post_data['post_type'] ) { return new IXR_Error( 401, __( 'The post type may not be changed.' ) ); } - } else { - if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( $post_type->cap->edit_posts ) ) { + } elseif ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( $post_type->cap->edit_posts ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to post on this site.' ) ); - } } switch ( $post_data['post_status'] ) { @@ -6071,10 +6069,8 @@ public function mw_editPost( $args ) { // Empty value deletes, non-empty value adds/updates. if ( empty( $content_struct['wp_post_thumbnail'] ) ) { delete_post_thumbnail( $post_id ); - } else { - if ( set_post_thumbnail( $post_id, $content_struct['wp_post_thumbnail'] ) === false ) { + } elseif ( set_post_thumbnail( $post_id, $content_struct['wp_post_thumbnail'] ) === false ) { return new IXR_Error( 404, __( 'Invalid attachment ID.' ) ); - } } unset( $content_struct['wp_post_thumbnail'] ); } From ad1c287f564166b9503a9ac34680ff71d0e5eb6d Mon Sep 17 00:00:00 2001 From: Igor Radovanov Date: Sun, 12 Oct 2025 22:27:27 +0200 Subject: [PATCH 3/3] This patch maintains backward compatibility with WordPress tests that expect sticky functions to work with non-existent post IDs --- src/wp-includes/post.php | 41 ++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 372090eddf8bb..68fb4ea473f34 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -644,7 +644,8 @@ function create_initial_post_types() { array( 'label' => _x( 'Published', 'post status' ), 'public' => true, - '_builtin' => true, /* internal use only. */ + '_builtin' => true, /* + internal use only. */ /* translators: %s: Number of published posts. */ 'label_count' => _n_noop( 'Published (%s)', @@ -658,7 +659,8 @@ function create_initial_post_types() { array( 'label' => _x( 'Scheduled', 'post status' ), 'protected' => true, - '_builtin' => true, /* internal use only. */ + '_builtin' => true, /* + internal use only. */ /* translators: %s: Number of scheduled posts. */ 'label_count' => _n_noop( 'Scheduled (%s)', @@ -672,7 +674,8 @@ function create_initial_post_types() { array( 'label' => _x( 'Draft', 'post status' ), 'protected' => true, - '_builtin' => true, /* internal use only. */ + '_builtin' => true, /* + internal use only. */ /* translators: %s: Number of draft posts. */ 'label_count' => _n_noop( 'Draft (%s)', @@ -687,7 +690,8 @@ function create_initial_post_types() { array( 'label' => _x( 'Pending', 'post status' ), 'protected' => true, - '_builtin' => true, /* internal use only. */ + '_builtin' => true, /* + internal use only. */ /* translators: %s: Number of pending posts. */ 'label_count' => _n_noop( 'Pending (%s)', @@ -702,7 +706,8 @@ function create_initial_post_types() { array( 'label' => _x( 'Private', 'post status' ), 'private' => true, - '_builtin' => true, /* internal use only. */ + '_builtin' => true, /* + internal use only. */ /* translators: %s: Number of private posts. */ 'label_count' => _n_noop( 'Private (%s)', @@ -716,7 +721,8 @@ function create_initial_post_types() { array( 'label' => _x( 'Trash', 'post status' ), 'internal' => true, - '_builtin' => true, /* internal use only. */ + '_builtin' => true, /* + internal use only. */ /* translators: %s: Number of trashed posts. */ 'label_count' => _n_noop( 'Trash (%s)', @@ -751,7 +757,8 @@ function create_initial_post_types() { array( 'label' => _x( 'Pending', 'request status' ), 'internal' => true, - '_builtin' => true, /* internal use only. */ + '_builtin' => true, /* + internal use only. */ /* translators: %s: Number of pending requests. */ 'label_count' => _n_noop( 'Pending (%s)', @@ -766,7 +773,8 @@ function create_initial_post_types() { array( 'label' => _x( 'Confirmed', 'request status' ), 'internal' => true, - '_builtin' => true, /* internal use only. */ + '_builtin' => true, /* + internal use only. */ /* translators: %s: Number of confirmed requests. */ 'label_count' => _n_noop( 'Confirmed (%s)', @@ -781,7 +789,8 @@ function create_initial_post_types() { array( 'label' => _x( 'Failed', 'request status' ), 'internal' => true, - '_builtin' => true, /* internal use only. */ + '_builtin' => true, /* + internal use only. */ /* translators: %s: Number of failed requests. */ 'label_count' => _n_noop( 'Failed (%s)', @@ -796,7 +805,8 @@ function create_initial_post_types() { array( 'label' => _x( 'Completed', 'request status' ), 'internal' => true, - '_builtin' => true, /* internal use only. */ + '_builtin' => true, /* + internal use only. */ /* translators: %s: Number of completed requests. */ 'label_count' => _n_noop( 'Completed (%s)', @@ -2860,6 +2870,9 @@ function is_sticky( $post_id = 0 ) { } $post = get_post( $post_id ); + if ( ! $post ) { + return false; + } // Check if the post type supports stickiness. if ( ! post_type_supports( $post->post_type, 'sticky' ) ) { @@ -3275,8 +3288,8 @@ function stick_post( $post_id ) { $post = get_post( $post_id ); - // Check if the post type supports stickiness. - if ( ! post_type_supports( $post->post_type, 'sticky' ) ) { + // Check if the post type supports stickiness (only for existing posts). + if ( $post && ! post_type_supports( $post->post_type, 'sticky' ) ) { return; } @@ -3319,8 +3332,8 @@ function unstick_post( $post_id ) { $post_id = (int) $post_id; $post = get_post( $post_id ); - // Check if the post type supports stickiness. - if ( ! post_type_supports( $post->post_type, 'sticky' ) ) { + // Check if the post type supports stickiness (only for existing posts). + if ( $post && ! post_type_supports( $post->post_type, 'sticky' ) ) { return; } $stickies = get_option( 'sticky_posts' );