Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
27ce87b
Add support for ActivityPub object post type
pfefferle Oct 13, 2025
fd3661e
Add ActivityPub content sanitizer and update usage
pfefferle Oct 13, 2025
5707420
Add HTML to blocks conversion and refactor sanitization
pfefferle Oct 13, 2025
630aec6
Remove get_node_attributes method and inline list attribute
pfefferle Oct 13, 2025
ca3305e
Rename html_to_blocks to convert_from_html in Blocks
pfefferle Oct 13, 2025
5ab729b
Apply suggestion from @Copilot
pfefferle Oct 13, 2025
e302e93
Apply suggestion from @Copilot
pfefferle Oct 13, 2025
955921c
Apply suggestion from @Copilot
pfefferle Oct 13, 2025
d0fa8e1
Add tests for content sanitization and block conversion
pfefferle Oct 13, 2025
7d6004e
Add tests for Create handler object sanitization and edge cases
pfefferle Oct 13, 2025
7466883
Add unit tests for Objects collection class
pfefferle Oct 13, 2025
b61302e
Add changelog
matticbot Oct 13, 2025
a400925
Refactor Update handler methods for clarity
pfefferle Oct 13, 2025
859b33b
Update @covers annotation to handle_actor_update
pfefferle Oct 13, 2025
9f43130
Update post type labels from 'Post' to 'Object'
pfefferle Oct 13, 2025
67932d8
Add missing newline after use statements
pfefferle Oct 13, 2025
7f07d36
Remove HTML to blocks conversion logic
pfefferle Oct 13, 2025
02528e1
Rename Objects collection to Posts throughout codebase
pfefferle Oct 13, 2025
f59a8f4
Fix comments and return types
obenland Oct 13, 2025
64d92ce
Rename create_object to create_post in Create handler
pfefferle Oct 13, 2025
94a9cc7
Update @covers annotation in test for create_post
pfefferle Oct 13, 2025
406d34e
Register post meta for remote actor ID
pfefferle Oct 13, 2025
3b345a0
Add 'ap_object_type' taxonomy to post type
pfefferle Oct 13, 2025
5dbbd42
Remove custom labels from taxonomy registration
pfefferle Oct 13, 2025
b5adfa3
Remove custom rewrite slugs from taxonomy registration
pfefferle Oct 13, 2025
f6dd350
Rename handle_actor_update to update_actor
pfefferle Oct 13, 2025
f784eb1
Rename test method and update covered function
pfefferle Oct 13, 2025
f9fc961
Refactor update handler result and docblock types
pfefferle Oct 13, 2025
a367a97
Improve error handling in Create and Update handlers
pfefferle Oct 13, 2025
c86fb3c
Refactor default error handling in handle_object_update
pfefferle Oct 13, 2025
ea44b16
Handle missing actor data in update handler
pfefferle Oct 13, 2025
df5afff
Refactor return values for create handler methods
pfefferle Oct 13, 2025
eb6c737
Merge branch 'trunk' into cpt-ap-post
pfefferle Oct 15, 2025
daa1ea4
Merge branch 'trunk' into cpt-ap-post
pfefferle Oct 16, 2025
f6a9aa6
Apply suggestion from @obenland
pfefferle Oct 16, 2025
0a323e3
Refactor post type and update handler method names
pfefferle Oct 16, 2025
77df3fc
Update post type registration method names in tests
pfefferle Oct 16, 2025
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/2311-from-description
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Added a new ap_object post type and taxonomies for storing and managing incoming ActivityPub objects, with updated handlers
61 changes: 61 additions & 0 deletions includes/class-post-types.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Activitypub\Collection\Followers;
use Activitypub\Collection\Inbox;
use Activitypub\Collection\Outbox;
use Activitypub\Collection\Posts;
use Activitypub\Collection\Remote_Actors;

/**
Expand All @@ -25,6 +26,7 @@ public static function init() {
\add_action( 'init', array( self::class, 'register_remote_actors_post_type' ), 11 );
\add_action( 'init', array( self::class, 'register_inbox_post_type' ), 11 );
\add_action( 'init', array( self::class, 'register_outbox_post_type' ), 11 );
\add_action( 'init', array( self::class, 'register_post_post_type' ), 11 );
\add_action( 'init', array( self::class, 'register_extra_fields_post_types' ), 11 );
\add_action( 'init', array( self::class, 'register_activitypub_post_meta' ), 11 );

Expand Down Expand Up @@ -345,6 +347,65 @@ public static function register_outbox_post_type() {
);
}

/**
* Register the Object post type.
*/
public static function register_post_post_type() {
\register_post_type(
Posts::POST_TYPE,
array(
'labels' => array(
'name' => \_x( 'Posts', 'post_type plural name', 'activitypub' ),
'singular_name' => \_x( 'Post', 'post_type single name', 'activitypub' ),
),
'capabilities' => array(
'create_posts' => false,
),
'map_meta_cap' => true,
'public' => false,
'show_in_rest' => true,
'rewrite' => false,
'query_var' => false,
'supports' => array( 'title', 'editor', 'author', 'custom-fields', 'excerpt', 'comments' ),
'delete_with_user' => true,
'can_export' => true,
'exclude_from_search' => true,
'taxonomies' => array( 'ap_tag', 'ap_object_type' ),
)
);

\register_taxonomy(
'ap_tag',
array( Posts::POST_TYPE ),
array(
'public' => false,
'query_var' => true,
'show_in_rest' => true,
)
);

\register_taxonomy(
'ap_object_type',
array( Posts::POST_TYPE ),
array(
'public' => false,
'query_var' => true,
'show_in_rest' => true,
)
);

\register_post_meta(
Posts::POST_TYPE,
'_activitypub_remote_actor_id',
array(
'type' => 'integer',
'single' => true,
'description' => 'The local ID of the remote actor that created the object.',
'sanitize_callback' => 'absint',
)
);
}

/**
* Register the Extra Fields post types.
*/
Expand Down
15 changes: 15 additions & 0 deletions includes/class-sanitize.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,19 @@ public static function webfinger( $value ) {

return $value;
}

/**
* Sanitize content for ActivityPub.
*
* @param string $content The content to convert.
*
* @return string The converted content.
*/
public static function content( $content ) {
$content = \make_clickable( $content );
$content = \wpautop( $content );
$content = \wp_kses_post( $content );

return $content;
}
}
5 changes: 5 additions & 0 deletions includes/collection/class-inbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
* @link https://www.w3.org/TR/activitypub/#inbox
*/
class Inbox {
/**
* The post type for the objects.
*
* @var string
*/
const POST_TYPE = 'ap_inbox';

/**
Expand Down
5 changes: 5 additions & 0 deletions includes/collection/class-outbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
* @link https://www.w3.org/TR/activitypub/#outbox
*/
class Outbox {
/**
* The post type for the objects.
*
* @var string
*/
const POST_TYPE = 'ap_outbox';

/**
Expand Down
167 changes: 167 additions & 0 deletions includes/collection/class-posts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php
/**
* Posts collection file.
*
* @package Activitypub
*/

namespace Activitypub\Collection;

use Activitypub\Sanitize;

use function Activitypub\object_to_uri;

/**
* Posts collection.
*
* Provides methods to retrieve, create, update, and manage ActivityPub posts (articles, notes, media, etc.).
*/
class Posts {
/**
* The post type for the posts.
*
* @var string
*/
const POST_TYPE = 'ap_post';

/**
* Add an object to the collection.
*
* @param array $activity The activity object data.
*
* @return \WP_Post|\WP_Error The object post or WP_Error on failure.
*/
public static function add( $activity ) {
$activity_object = $activity['object'];
$actor = Remote_Actors::fetch_by_uri( object_to_uri( $activity_object['attributedTo'] ) );

if ( \is_wp_error( $actor ) ) {
return $actor;
}

$post_array = self::activity_to_post( $activity_object );
$post_id = \wp_insert_post( $post_array, true );

if ( \is_wp_error( $post_id ) ) {
return $post_id;
}

\add_post_meta( $post_id, '_activitypub_remote_actor_id', $actor->ID );

self::add_taxonomies( $post_id, $activity_object );

return \get_post( $post_id );
}

/**
* Get an object from the collection.
*
* @param int $id The object ID.
*
* @return \WP_Post|null The post object or null on failure.
*/
public static function get( $id ) {
return \get_post( $id );
}

/**
* Get an object by its GUID.
*
* @param string $guid The object GUID.
*
* @return \WP_Post|\WP_Error The object post or WP_Error on failure.
*/
public static function get_by_guid( $guid ) {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$post_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT ID FROM $wpdb->posts WHERE guid=%s AND post_type=%s",
\esc_url( $guid ),
self::POST_TYPE
)
);

if ( ! $post_id ) {
return new \WP_Error(
'activitypub_object_not_found',
\__( 'Object not found', 'activitypub' ),
array( 'status' => 404 )
);
}

return \get_post( $post_id );
}

/**
* Update an object in the collection.
*
* @param array $activity The activity object data.
*
* @return \WP_Post|\WP_Error The updated object post or WP_Error on failure.
*/
public static function update( $activity ) {
$post = self::get_by_guid( $activity['object']['id'] );
if ( \is_wp_error( $post ) ) {
return $post;
}

$post_array = self::activity_to_post( $activity['object'] );
$post_array['ID'] = $post->ID;
$post_id = \wp_update_post( $post_array, true );

if ( \is_wp_error( $post_id ) ) {
return $post_id;
}

self::add_taxonomies( $post_id, $activity['object'] );

return \get_post( $post_id );
}

/**
* Convert an activity to a post array.
*
* @param array $activity The activity array.
*
* @return array|\WP_Error The post array or WP_Error on failure.
*/
private static function activity_to_post( $activity ) {
if ( ! is_array( $activity ) ) {
return new \WP_Error( 'invalid_activity', __( 'Invalid activity format', 'activitypub' ) );
}

return array(
'post_title' => isset( $activity['name'] ) ? \wp_strip_all_tags( $activity['name'] ) : '',
'post_content' => isset( $activity['content'] ) ? Sanitize::content( $activity['content'] ) : '',
'post_excerpt' => isset( $activity['summary'] ) ? \wp_strip_all_tags( $activity['summary'] ) : '',
'post_status' => 'publish',
'post_type' => self::POST_TYPE,
'guid' => isset( $activity['id'] ) ? \esc_url_raw( $activity['id'] ) : '',
);
}

/**
* Add taxonomies to the object post.
*
* @param int $post_id The post ID.
* @param array $activity_object The activity object data.
*/
private static function add_taxonomies( $post_id, $activity_object ) {
// Save Object Type as Taxonomy item.
\wp_set_post_terms( $post_id, array( $activity_object['type'] ), 'ap_object_type' );

$tags = array();

// Save the Hashtags as Taxonomy items.
if ( ! empty( $activity_object['tag'] ) && \is_array( $activity_object['tag'] ) ) {
foreach ( $activity_object['tag'] as $tag ) {
if ( isset( $tag['type'] ) && 'Hashtag' === $tag['type'] && isset( $tag['name'] ) ) {
$tags[] = \wp_strip_all_tags( ltrim( $tag['name'], '#' ) );
}
}
}

\wp_set_post_terms( $post_id, $tags, 'ap_tag' );
}
}
29 changes: 26 additions & 3 deletions includes/debug.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use Activitypub\Collection\Inbox;
use Activitypub\Collection\Outbox;
use Activitypub\Collection\Posts;

/**
* Allow localhost URLs if WP_DEBUG is true.
Expand All @@ -32,8 +33,8 @@ function allow_localhost( $parsed_args ) {
*
* @return array The arguments for the post type.
*/
function debug_outbox_post_type( $args, $post_type ) {
if ( ! \in_array( $post_type, array( Outbox::POST_TYPE, Inbox::POST_TYPE ), true ) ) {
function debug_post_type( $args, $post_type ) {
if ( ! \in_array( $post_type, array( Outbox::POST_TYPE, Inbox::POST_TYPE, Posts::POST_TYPE ), true ) ) {
return $args;
}

Expand All @@ -43,11 +44,33 @@ function debug_outbox_post_type( $args, $post_type ) {
$args['menu_icon'] = 'dashicons-upload';
} elseif ( Inbox::POST_TYPE === $post_type ) {
$args['menu_icon'] = 'dashicons-download';
} elseif ( Posts::POST_TYPE === $post_type ) {
$args['menu_icon'] = 'dashicons-media-document';
}

return $args;
}
\add_filter( 'register_post_type_args', '\Activitypub\debug_post_type', 10, 2 );

/**
* Debug the object type taxonomy.
*
* @param array $args The arguments for the taxonomy.
* @param string $taxonomy The taxonomy.
*
* @return array The arguments for the taxonomy.
*/
function debug_taxonomy( $args, $taxonomy ) {
if ( ! in_array( $taxonomy, array( 'ap_object_type', 'ap_tag' ), true ) ) {
return $args;
}

$args['show_ui'] = true;
$args['show_in_menu'] = true;

return $args;
}
\add_filter( 'register_post_type_args', '\Activitypub\debug_outbox_post_type', 10, 2 );
\add_filter( 'register_taxonomy_args', '\Activitypub\debug_taxonomy', 10, 2 );

/**
* Debug the outbox post type column.
Expand Down
Loading