From cd774e9fcd7683d988815704147c1832f585dc2a Mon Sep 17 00:00:00 2001 From: Wil Gerken Date: Thu, 5 Mar 2026 15:48:12 -0700 Subject: [PATCH 1/5] fix(content-distribution): keep node post in status_on_publish when hub schedules --- .../class-incoming-post.php | 19 +++++- .../test-incoming-post.php | 59 +++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/includes/content-distribution/class-incoming-post.php b/includes/content-distribution/class-incoming-post.php index ef9fdb2..3c887d8 100644 --- a/includes/content-distribution/class-incoming-post.php +++ b/includes/content-distribution/class-incoming-post.php @@ -683,8 +683,11 @@ public function insert( $payload = [] ) { * Post status handling. * * If post is being published, use the incoming or stored - * `status_on_publish` if available. Otherwise, use the post status from - * the payload. + * `status_on_publish` if available. If post is being scheduled + * (future) and `status_on_publish` is a non-publish status, keep + * the node post in that status. This prevents WP cron from + * scheduling `publish_future_post` and auto-publishing the node + * post. Otherwise, use the post status from the payload. */ if ( $post_data['post_status'] === 'publish' ) { if ( $is_new_post ) { @@ -695,6 +698,18 @@ public function insert( $payload = [] ) { $postarr['post_status'] = $status_on_publish; } } + } elseif ( $post_data['post_status'] === 'future' ) { + if ( $is_new_post ) { + $status_on_publish = $this->payload['status_on_publish']; + } else { + $status_on_publish = get_post_meta( $this->ID, self::STATUS_ON_PUBLISH_META, true ); + } + if ( $status_on_publish && 'publish' !== $status_on_publish ) { + $postarr['post_status'] = $status_on_publish; + } else { + // If status_on_publish is 'publish' or unset, mirror the hub's schedule. + $postarr['post_status'] = 'future'; + } } else { $postarr['post_status'] = $post_data['post_status']; } diff --git a/tests/unit-tests/content-distribution/test-incoming-post.php b/tests/unit-tests/content-distribution/test-incoming-post.php index b5d0250..c394c22 100644 --- a/tests/unit-tests/content-distribution/test-incoming-post.php +++ b/tests/unit-tests/content-distribution/test-incoming-post.php @@ -524,6 +524,65 @@ public function test_pending_distribution() { $this->assertSame( 'publish', get_post_status( $post_id ) ); } + /** + * Test that a scheduled (future) hub post does not auto-publish on the node + * when status_on_publish is set to a non-publish status. + * + * Regression test for: hub distributes a draft, then schedules it shortly + * after. The scheduling sync sends post_status='future' to the node. If the + * node applies that status directly, WordPress cron will find a future-status + * post and auto-publish it via wp_publish_post(), bypassing the + * status_on_publish setting entirely. + * + * The node should keep the post in the status_on_publish state, such as draft, + * so that WP cron never has a chance to publish it. + */ + public function test_future_status_with_non_publish_status_on_publish() { + $payload = $this->get_sample_payload(); + + // Simulate Event 1: hub distributes the post as a draft. + // status_on_publish='draft' means the node should never auto-publish. + $payload['post_data']['post_status'] = 'draft'; + $payload['status_on_publish'] = 'draft'; + + $post_id = $this->incoming_post->insert( $payload ); + $this->assertSame( 'draft', get_post_status( $post_id ) ); + + // Simulate Event 2: hub schedules the post, syncing post_status='future'. + // The node must not apply 'future', as a future-status post with a past date + // is auto-published by WP cron, bypassing status_on_publish entirely. + $payload['post_data']['post_status'] = 'future'; + $this->incoming_post->insert( $payload ); + + $this->assertSame( 'draft', get_post_status( $post_id ) ); + } + + /** + * Test that a scheduled (future) hub post mirrors the future status on the + * node when status_on_publish is 'publish'. + * + * When a publisher wants the node to publish in sync with the hub, the node + * should mirror the 'future' status so that WP cron fires on both sites at + * the same scheduled time. + */ + public function test_future_status_with_publish_status_on_publish() { + $payload = $this->get_sample_payload(); + + $payload['post_data']['post_status'] = 'draft'; + $payload['status_on_publish'] = 'publish'; + + $post_id = $this->incoming_post->insert( $payload ); + + // Hub schedules the post. date_gmt must be in the future or WordPress + // will immediately publish the post rather than storing it as 'future'. + $payload['post_data']['post_status'] = 'future'; + $payload['post_data']['date_gmt'] = gmdate( 'Y-m-d H:i:s', strtotime( '+1 week' ) ); + $this->incoming_post->insert( $payload ); + + // Node should mirror the hub's scheduled status so both publish together. + $this->assertSame( 'future', get_post_status( $post_id ) ); + } + /** * Test that "status on publish" only applies once. */ From c6789a867fdb219b8657511f5380b94143524511 Mon Sep 17 00:00:00 2001 From: Wil Gerken Date: Thu, 5 Mar 2026 16:30:23 -0700 Subject: [PATCH 2/5] fix(content-distribution): guard against missing status_on_publish key --- includes/content-distribution/class-incoming-post.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/includes/content-distribution/class-incoming-post.php b/includes/content-distribution/class-incoming-post.php index 3c887d8..2058bae 100644 --- a/includes/content-distribution/class-incoming-post.php +++ b/includes/content-distribution/class-incoming-post.php @@ -691,7 +691,7 @@ public function insert( $payload = [] ) { */ if ( $post_data['post_status'] === 'publish' ) { if ( $is_new_post ) { - $postarr['post_status'] = $this->payload['status_on_publish']; + $postarr['post_status'] = isset( $this->payload['status_on_publish'] ) ? $this->payload['status_on_publish'] : ''; } else { $status_on_publish = get_post_meta( $this->ID, self::STATUS_ON_PUBLISH_META, true ); if ( $status_on_publish ) { @@ -700,10 +700,15 @@ public function insert( $payload = [] ) { } } elseif ( $post_data['post_status'] === 'future' ) { if ( $is_new_post ) { - $status_on_publish = $this->payload['status_on_publish']; + if ( isset( $this->payload['status_on_publish'] ) ) { + $status_on_publish = $this->payload['status_on_publish']; + } else { + $status_on_publish = ''; + } } else { $status_on_publish = get_post_meta( $this->ID, self::STATUS_ON_PUBLISH_META, true ); } + if ( $status_on_publish && 'publish' !== $status_on_publish ) { $postarr['post_status'] = $status_on_publish; } else { From b0dda0a4d9d14ead85d2e6e3dd146a8d35c25f1a Mon Sep 17 00:00:00 2001 From: Wil Gerken Date: Thu, 5 Mar 2026 17:34:38 -0700 Subject: [PATCH 3/5] fix(content-distribution): guard against missing status_on_publish key in payload --- .../class-incoming-post.php | 17 +++++++----- .../test-incoming-post.php | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/includes/content-distribution/class-incoming-post.php b/includes/content-distribution/class-incoming-post.php index 2058bae..12d8716 100644 --- a/includes/content-distribution/class-incoming-post.php +++ b/includes/content-distribution/class-incoming-post.php @@ -691,7 +691,7 @@ public function insert( $payload = [] ) { */ if ( $post_data['post_status'] === 'publish' ) { if ( $is_new_post ) { - $postarr['post_status'] = isset( $this->payload['status_on_publish'] ) ? $this->payload['status_on_publish'] : ''; + $postarr['post_status'] = isset( $this->payload['status_on_publish'] ) ? $this->payload['status_on_publish'] : $post_data['post_status']; } else { $status_on_publish = get_post_meta( $this->ID, self::STATUS_ON_PUBLISH_META, true ); if ( $status_on_publish ) { @@ -763,11 +763,16 @@ public function insert( $payload = [] ) { // Handle `status_on_publish` meta. if ( $post_data['post_status'] !== 'publish' && $is_new_post ) { // Store the publish status for new posts. - update_post_meta( - $post_id, - self::STATUS_ON_PUBLISH_META, - $this->payload['status_on_publish'] - ); + if ( isset( $this->payload['status_on_publish'] ) ) { + // Only store the meta if the key is present. An absent status_on_publish + // means no override was configured — the node will fall back to safe + // defaults when the hub later sends 'publish' or 'future'. + update_post_meta( + $post_id, + self::STATUS_ON_PUBLISH_META, + $this->payload['status_on_publish'] + ); + } } elseif ( $post_data['post_status'] === 'publish' && ! $is_new_post ) { // Clean up the meta for published posts so it's not re-published after // being unpublished. diff --git a/tests/unit-tests/content-distribution/test-incoming-post.php b/tests/unit-tests/content-distribution/test-incoming-post.php index c394c22..587e16f 100644 --- a/tests/unit-tests/content-distribution/test-incoming-post.php +++ b/tests/unit-tests/content-distribution/test-incoming-post.php @@ -583,6 +583,33 @@ public function test_future_status_with_publish_status_on_publish() { $this->assertSame( 'future', get_post_status( $post_id ) ); } + /** + * Test that a scheduled (future) hub post mirrors the future status on the + * node when status_on_publish is absent from the payload. + * + * The sample payload includes status_on_publish by default, so it is + * explicitly unset here to simulate a payload that omits the key. + * The node should fall back to mirroring the hub's future status. + */ + public function test_future_status_with_unset_status_on_publish() { + $payload = $this->get_sample_payload(); + + // Remove status_on_publish to simulate a payload that omits the key. + unset( $payload['status_on_publish'] ); + + $payload['post_data']['post_status'] = 'draft'; + $post_id = $this->incoming_post->insert( $payload ); + + // Hub schedules the post. date_gmt must be in the future or WordPress + // will immediately publish the post rather than storing it as 'future'. + $payload['post_data']['post_status'] = 'future'; + $payload['post_data']['date_gmt'] = gmdate( 'Y-m-d H:i:s', strtotime( '+1 week' ) ); + $this->incoming_post->insert( $payload ); + + // With no status_on_publish set, the node should mirror the hub's schedule. + $this->assertSame( 'future', get_post_status( $post_id ) ); + } + /** * Test that "status on publish" only applies once. */ From 131256fbd7373d4b2dd09d3fcb666c5aecc86405 Mon Sep 17 00:00:00 2001 From: Wil Gerken Date: Thu, 5 Mar 2026 17:44:14 -0700 Subject: [PATCH 4/5] fix(content-distribution): remove trailing whitespace in docblock --- tests/unit-tests/content-distribution/test-incoming-post.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit-tests/content-distribution/test-incoming-post.php b/tests/unit-tests/content-distribution/test-incoming-post.php index 587e16f..4426bdc 100644 --- a/tests/unit-tests/content-distribution/test-incoming-post.php +++ b/tests/unit-tests/content-distribution/test-incoming-post.php @@ -588,7 +588,7 @@ public function test_future_status_with_publish_status_on_publish() { * node when status_on_publish is absent from the payload. * * The sample payload includes status_on_publish by default, so it is - * explicitly unset here to simulate a payload that omits the key. + * explicitly unset here to simulate a payload that omits the key. * The node should fall back to mirroring the hub's future status. */ public function test_future_status_with_unset_status_on_publish() { From 0c0e0abec124e4998a05edc96269425b872c455e Mon Sep 17 00:00:00 2001 From: Wil Gerken Date: Thu, 5 Mar 2026 17:56:18 -0700 Subject: [PATCH 5/5] docs(content-distribution): clarify docblock for publish status handling --- .../content-distribution/class-incoming-post.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/includes/content-distribution/class-incoming-post.php b/includes/content-distribution/class-incoming-post.php index 12d8716..465c2c9 100644 --- a/includes/content-distribution/class-incoming-post.php +++ b/includes/content-distribution/class-incoming-post.php @@ -682,12 +682,15 @@ public function insert( $payload = [] ) { /** * Post status handling. * - * If post is being published, use the incoming or stored - * `status_on_publish` if available. If post is being scheduled - * (future) and `status_on_publish` is a non-publish status, keep - * the node post in that status. This prevents WP cron from - * scheduling `publish_future_post` and auto-publishing the node - * post. Otherwise, use the post status from the payload. + * If post is being published, apply the stored `status_on_publish` + * override if one exists. For new posts, use the incoming payload + * value instead. If no override is configured, the post status is + * left unchanged for existing posts and defaults to the incoming + * status for new ones. If post is being scheduled (future) and + * `status_on_publish` is a non-publish status, keep the node post + * in that status. This prevents WP cron from scheduling + * `publish_future_post` and auto-publishing the node post. + * Otherwise, use the post status from the payload. */ if ( $post_data['post_status'] === 'publish' ) { if ( $is_new_post ) {