Skip to content
Merged
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
46 changes: 37 additions & 9 deletions includes/content-distribution/class-incoming-post.php
Original file line number Diff line number Diff line change
Expand Up @@ -682,19 +682,42 @@ 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.
* 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 ) {
$postarr['post_status'] = $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 ) {
$postarr['post_status'] = $status_on_publish;
}
}
} elseif ( $post_data['post_status'] === 'future' ) {
if ( $is_new_post ) {
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 {
// 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'];
}
Expand Down Expand Up @@ -743,11 +766,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.
Expand Down
86 changes: 86 additions & 0 deletions tests/unit-tests/content-distribution/test-incoming-post.php
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,92 @@ 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 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.
*/
Expand Down