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..c8eb9dcb31aa5 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.
@@ -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'] );
}
@@ -6363,7 +6359,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..68fb4ea473f34 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',
@@ -643,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)',
@@ -657,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)',
@@ -671,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)',
@@ -686,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)',
@@ -701,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)',
@@ -715,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)',
@@ -750,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)',
@@ -765,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)',
@@ -780,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)',
@@ -795,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)',
@@ -2858,6 +2869,16 @@ function is_sticky( $post_id = 0 ) {
$post_id = get_the_ID();
}
+ $post = get_post( $post_id );
+ if ( ! $post ) {
+ return false;
+ }
+
+ // 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 +3284,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 (only for existing posts).
+ if ( $post && ! post_type_supports( $post->post_type, 'sticky' ) ) {
+ return;
+ }
+
$stickies = get_option( 'sticky_posts' );
$updated = false;
@@ -3300,7 +3329,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 (only for existing posts).
+ if ( $post && ! 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' ) );
+ }
+}