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' )`.
40 changes: 23 additions & 17 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 @@ -261,16 +261,14 @@ public function on_status_change( string $new_status, string $old_status, \WP_Po
}

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

Expand Down Expand Up @@ -325,12 +323,12 @@ public function on_before_delete( int $post_id ): void {
}

/*
* No allowlist check here. Permanent delete is a cleanup path: if
* 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 dropped from the syncable allowlist.
* Gating this on the current allowlist would orphan already-
* published records whenever a site narrows its configuration.
* 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 @@ -368,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 @@ -382,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();

Comment thread
pfefferle marked this conversation as resolved.
/*
* 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(
Comment thread
pfefferle marked this conversation as resolved.
Comment thread
kraftbj marked this conversation as resolved.
'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 );

Comment thread
pfefferle marked this conversation as resolved.
/**
* 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 );
}
88 changes: 86 additions & 2 deletions includes/wp-admin/class-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@

\defined( 'ABSPATH' ) || exit;

use Atmosphere\Backfill;
use Atmosphere\OAuth\Client;
use Atmosphere\Post_Types;
use Atmosphere\Publisher;
use function Atmosphere\get_connection;
use function Atmosphere\get_supported_post_types;
use function Atmosphere\is_connected;

/**
Expand Down Expand Up @@ -68,6 +69,23 @@ public static function register_settings(): void {
)
);

\register_setting(
'atmosphere',
'atmosphere_support_post_types',
array(
'type' => 'array',
'description' => 'Post types to publish to AT Protocol.',
'default' => array( 'post' ),
'sanitize_callback' => array( Post_Types::class, 'sanitize' ),
'show_in_rest' => array(
'schema' => array(
'type' => 'array',
'items' => array( 'type' => 'string' ),
),
),
)
);

\register_setting(
'atmosphere',
'atmosphere_handle',
Expand Down Expand Up @@ -116,6 +134,14 @@ public static function register_settings(): void {
'atmosphere_publishing'
);

\add_settings_field(
'atmosphere_support_post_types',
\__( 'Post types', 'atmosphere' ),
array( self::class, 'render_support_post_types_field' ),
'atmosphere',
'atmosphere_publishing'
);

\add_settings_field(
'atmosphere_backfill',
\__( 'Backfill', 'atmosphere' ),
Expand Down Expand Up @@ -247,6 +273,64 @@ public static function render_auto_publish_field(): void {
<?php
}

/**
* Render the post type support checkboxes.
*/
public static function render_support_post_types_field(): void {
$post_types = \get_post_types( array( 'public' => true ), 'objects' );

/*
* The checkbox state reflects the saved option only. Native
* `add_post_type_support()` opt-ins and the syncable filter are
* surfaced as a note below the label so the user can see when a
* post type is enabled outside this UI.
*/
$saved = (array) \get_option( 'atmosphere_support_post_types', array( 'post' ) );
$saved = \array_filter( \array_map( 'sanitize_key', $saved ) );
$effective = get_supported_post_types();
?>
<fieldset>
<legend class="screen-reader-text">
<?php \esc_html_e( 'Post types', 'atmosphere' ); ?>
</legend>
<?php
foreach ( $post_types as $post_type ) :
$is_saved = \in_array( $post_type->name, $saved, true );
$is_effective = \in_array( $post_type->name, $effective, true );
$is_external = ! $is_saved && $is_effective;
$is_filtered_out = $is_saved && ! $is_effective;
?>
<p>
<label>
<input
type="checkbox"
name="atmosphere_support_post_types[]"
value="<?php echo \esc_attr( $post_type->name ); ?>"
<?php \checked( $is_saved ); ?>
>
<?php echo \esc_html( $post_type->label ); ?>
<code><?php echo \esc_html( $post_type->name ); ?></code>
</label>
Comment thread
pfefferle marked this conversation as resolved.
<?php if ( $is_external ) : ?>
<br>
<span class="description">
<?php \esc_html_e( 'Enabled by another plugin or theme.', 'atmosphere' ); ?>
</span>
<?php elseif ( $is_filtered_out ) : ?>
<br>
<span class="description">
<?php \esc_html_e( 'Disabled by another plugin or theme — this post type will not be published.', 'atmosphere' ); ?>
</span>
<?php endif; ?>
</p>
<?php endforeach; ?>
</fieldset>
<p class="description">
<?php \esc_html_e( 'Select which post types are published to AT Protocol.', 'atmosphere' ); ?>
</p>
<?php
}

/**
* Render the Backfill field.
*/
Expand Down Expand Up @@ -399,7 +483,7 @@ public static function add_meta_box(): void {
return;
}

foreach ( Backfill::syncable_post_types() as $post_type ) {
foreach ( get_supported_post_types() as $post_type ) {
\add_meta_box(
'atmosphere',
\__( 'ATmosphere', 'atmosphere' ),
Expand Down
Loading