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
4 changes: 4 additions & 0 deletions .github/changelog/add-post-type-support
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Choose which post types are published to AT Protocol from the ATmosphere settings page. Plugins and themes can also opt their custom post types in directly with `add_post_type_support( 'your_type', 'atmosphere' )`.
4 changes: 4 additions & 0 deletions .github/changelog/fix-cleanup-gate-split
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Preserve remote cleanup of already-synced posts when their post type is removed from the syncable allowlist.
76 changes: 56 additions & 20 deletions includes/class-atmosphere.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public function output_document_link(): void {
return;
}

if ( ! \in_array( $post->post_type, Backfill::syncable_post_types(), true ) ) {
if ( ! is_supported_post_type( $post->post_type ) ) {
return;
}

Expand Down Expand Up @@ -220,7 +220,7 @@ public function preview(): void {
return;
}

if ( ! \in_array( $post->post_type, Backfill::syncable_post_types(), true ) ) {
if ( ! is_supported_post_type( $post->post_type ) ) {
\status_header( 404 );
exit;
}
Expand Down Expand Up @@ -251,35 +251,55 @@ public function on_status_change( string $new_status, string $old_status, \WP_Po
return;
}

if ( ! \in_array( $post->post_type, Backfill::syncable_post_types(), true ) ) {
$is_new_publish = 'publish' === $new_status && 'publish' !== $old_status;
$is_update = 'publish' === $new_status && 'publish' === $old_status;
$is_unpublish = 'publish' === $old_status && 'publish' !== $new_status;
Comment thread
pfefferle marked this conversation as resolved.

if ( ! $is_new_publish && ! $is_update && ! $is_unpublish ) {
// Transition between two non-publish states; nothing to schedule.
return;
}

/*
* Publish-time decisions respect the supported list so sites
* only sync the post types they've opted into. Unpublish is a
* cleanup path for records that were already synced, so
* narrowing support later must not orphan those remote records:
* unpublish defers to publication metadata (TIDs on the post)
* instead of the current support list.
*/
if ( ( $is_new_publish || $is_update ) && ! is_supported_post_type( $post->post_type ) ) {
return;
}

if ( $is_unpublish ) {
$bsky_tid = \get_post_meta( $post->ID, Transformer\Post::META_TID, true );
$doc_tid = \get_post_meta( $post->ID, Transformer\Document::META_TID, true );
if ( ! $bsky_tid && ! $doc_tid ) {
// Unpublish of a post that was never synced — nothing to clean up.
return;
}
}

// Prevent infinite loops from meta updates.
if ( \did_action( 'atmosphere_publishing' ) ) {
return;
}

\do_action( 'atmosphere_publishing' );

if ( 'publish' === $new_status && 'publish' !== $old_status ) {
// New publish — schedule async.
if ( $is_new_publish ) {
\wp_schedule_single_event( \time(), 'atmosphere_publish_post', array( $post->ID ) );
} elseif ( 'publish' === $new_status && 'publish' === $old_status ) {
// Update.
} elseif ( $is_update ) {
\wp_schedule_single_event( \time(), 'atmosphere_update_post', array( $post->ID ) );
} elseif ( 'publish' === $old_status && 'publish' !== $new_status ) {
} else {
/*
* Genuine unpublish — transitioning away from publish.
* Use atmosphere_delete_post (not delete_records) so that
* post meta is cleaned up on success, allowing a subsequent
* restore (trash → publish) to republish correctly.
* Genuine unpublish — use atmosphere_delete_post (not
* delete_records) so post meta is cleaned up on success,
* allowing a subsequent restore (trash → publish) to
* republish correctly.
*/
$bsky_tid = \get_post_meta( $post->ID, Transformer\Post::META_TID, true );
$doc_tid = \get_post_meta( $post->ID, Transformer\Document::META_TID, true );
if ( $bsky_tid || $doc_tid ) {
\wp_schedule_single_event( \time(), 'atmosphere_delete_post', array( $post->ID ) );
}
\wp_schedule_single_event( \time(), 'atmosphere_delete_post', array( $post->ID ) );
}
}

Expand All @@ -298,10 +318,18 @@ public function on_before_delete( int $post_id ): void {

$post = \get_post( $post_id );

if ( ! $post || ! \in_array( $post->post_type, Backfill::syncable_post_types(), true ) ) {
if ( ! $post ) {
return;
}

/*
* No support check here. Permanent delete is a cleanup path: if
* the post has Atmosphere publication metadata it was synced at
* some point, and the remote records must be removed even if the
* post type has since been removed from the supported list.
* Gating this on current support would orphan already-published
* records whenever a site narrows its configuration.
*/
$bsky_tid = \get_post_meta( $post_id, Transformer\Post::META_TID, true );
$doc_tid = \get_post_meta( $post_id, Transformer\Document::META_TID, true );

Expand Down Expand Up @@ -338,11 +366,19 @@ public function cron_refresh_token(): void {
* Register async action hooks (called by WP-Cron).
*/
public static function register_async_hooks(): void {
/*
* Publish/update cron callbacks re-check post-type support.
* A user (or downstream filter) can disable a post type after a
* cron event was queued, and we must not still publish it.
*
* The delete callback intentionally skips this check so cleanup
* still runs after support is removed.
*/
\add_action(
'atmosphere_publish_post',
static function ( int $post_id ): void {
$post = \get_post( $post_id );
if ( $post && 'publish' === $post->post_status ) {
if ( $post && 'publish' === $post->post_status && is_supported_post_type( $post->post_type ) ) {
Publisher::publish( $post );
}
}
Expand All @@ -352,7 +388,7 @@ static function ( int $post_id ): void {
'atmosphere_update_post',
static function ( int $post_id ): void {
$post = \get_post( $post_id );
if ( $post && 'publish' === $post->post_status ) {
if ( $post && 'publish' === $post->post_status && is_supported_post_type( $post->post_type ) ) {
Publisher::update( $post );
}
}
Expand Down
36 changes: 19 additions & 17 deletions includes/class-backfill.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,21 @@ public static function handle_count(): void {

\check_ajax_referer( 'atmosphere_backfill', 'nonce' );

$post_types = self::syncable_post_types();
$post_types = get_supported_post_types();

/*
* Short-circuit when no post types are enabled. Passing an empty
* array to get_posts() falls back to the default `post` query,
* which would surface posts that nothing is configured to sync.
*/
if ( empty( $post_types ) ) {
\wp_send_json_success(
array(
'total' => 0,
'post_ids' => array(),
)
);
}

/**
* Filters the maximum number of posts to backfill.
Expand Down Expand Up @@ -99,12 +113,14 @@ public static function handle_batch(): void {
\wp_send_json_error( 'No post IDs provided.' );
}

$results = array();
// Resolve supported post types once for the whole batch.
$supported = get_supported_post_types();
$results = array();

foreach ( $post_ids as $post_id ) {
$post = \get_post( $post_id );

if ( ! $post || 'publish' !== $post->post_status ) {
if ( ! $post || 'publish' !== $post->post_status || ! \in_array( $post->post_type, $supported, true ) ) {
$results[] = array(
'id' => $post_id,
'success' => false,
Expand Down Expand Up @@ -133,18 +149,4 @@ public static function handle_batch(): void {

\wp_send_json_success( array( 'results' => $results ) );
}

/**
* Get the post types eligible for syncing.
*
* @return string[]
*/
public static function syncable_post_types(): array {
/**
* Filters the post types that can be synced to AT Protocol.
*
* @param string[] $post_types Post type slugs.
*/
return \apply_filters( 'atmosphere_syncable_post_types', array( 'post' ) );
}
}
82 changes: 82 additions & 0 deletions includes/class-post-types.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php
/**
* Post type support resolution and sanitization.
*
* @package Atmosphere
*/

namespace Atmosphere;

\defined( 'ABSPATH' ) || exit;

/**
* Post Types class.
*/
class Post_Types {

/**
* Resolve the full set of supported post types.
*
* Combines the configured option with anything third parties opted
* in via `\add_post_type_support( $post_type, 'atmosphere' )`.
*
* @return string[]
*/
public static function get_supported(): array {
$configured = (array) \get_option( 'atmosphere_support_post_types', array( 'post' ) );
$native = \get_post_types_by_support( 'atmosphere' );

$post_types = \array_merge( $configured, $native );

/**
* Filters the post types that support ATmosphere publishing.
*
* Runs after the option and native `add_post_type_support()`
* opt-ins are merged, so plugins can add or remove either source.
*
* @param string[] $post_types Post type slugs.
*/
$post_types = (array) \apply_filters( 'atmosphere_syncable_post_types', $post_types );

// Normalise: drop empties / non-strings, dedupe, re-index.
$post_types = \array_filter( $post_types, '\is_string' );
$post_types = \array_map( 'sanitize_key', $post_types );
$post_types = \array_filter( $post_types );

return \array_values( \array_unique( $post_types ) );
}

/**
* Whether a post type is supported.
*
* @param string $post_type Post type slug.
* @return bool
*/
public static function supports( string $post_type ): bool {
return \in_array( $post_type, self::get_supported(), true );
}

/**
* Sanitize the option on save.
*
* Normalises input to a clean string[] of unique public post type
* slugs. Coerces empty input (e.g. all checkboxes unchecked) to an
* empty array.
*
* @param mixed $value Submitted value.
* @return string[]
*/
public static function sanitize( $value ): array {
if ( empty( $value ) ) {
return array();
}

$allowed = \get_post_types( array( 'public' => true ) );

$value = \array_filter( (array) $value, '\is_string' );
$value = \array_map( 'sanitize_key', $value );
$value = \array_filter( $value );

return \array_values( \array_unique( \array_intersect( $value, $allowed ) ) );
}
}
19 changes: 19 additions & 0 deletions includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,22 @@ function get_did(): string {
function get_pds_endpoint(): string {
return get_connection()['pds_endpoint'] ?? '';
}

/**
* Get post types that publish to AT Protocol.
*
* @return string[] Post type slugs.
*/
function get_supported_post_types(): array {
return Post_Types::get_supported();
}

/**
* Whether a post type publishes to AT Protocol.
*
* @param string $post_type Post type slug.
* @return bool
*/
function is_supported_post_type( string $post_type ): bool {
return Post_Types::supports( $post_type );
}
Loading