Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 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
83ffc99
Add object deletion methods and update delete handler
pfefferle Oct 13, 2025
02528e1
Rename Objects collection to Posts throughout codebase
pfefferle Oct 13, 2025
a62a9f5
Rename class-objects.php to class-posts.php and update usage
pfefferle Oct 13, 2025
23b6d66
Merge branch 'cpt-ap-post' into delete-reader-cpt
pfefferle Oct 13, 2025
6658609
Refactor delete handler for object and actor separation
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
8a12680
Merge branch 'cpt-ap-post' into delete-reader-cpt
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
c3eb52e
Merge branch 'cpt-ap-post' into delete-reader-cpt
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
10c7ef5
Add deletion of posts by remote actor on actor removal
pfefferle Oct 15, 2025
cf5a1b9
Merge branch 'cpt-ap-post' into delete-reader-cpt
pfefferle Oct 15, 2025
49e7dbb
Merge branch 'trunk' into delete-reader-cpt
pfefferle Oct 16, 2025
8891d07
Merge branch 'trunk' into delete-reader-cpt
pfefferle Oct 17, 2025
860e6ee
Merge branch 'trunk' into delete-reader-cpt
pfefferle Oct 18, 2025
7cd21da
Rename delete actor hooks and handler methods for clarity
pfefferle Oct 20, 2025
6a2448d
Merge branch 'trunk' into delete-reader-cpt
pfefferle Oct 20, 2025
707e75b
Merge branch 'trunk' into delete-reader-cpt
pfefferle Oct 20, 2025
b8e3d7a
Merge branch 'trunk' into delete-reader-cpt
pfefferle Oct 20, 2025
f2c713d
Merge branch 'trunk' into delete-reader-cpt
pfefferle Oct 20, 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
2 changes: 1 addition & 1 deletion includes/class-post-types.php
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ public static function register_outbox_post_type() {
}

/**
* Register the Object post type.
* Register the Post post type.
*/
public static function register_post_post_type() {
\register_post_type(
Expand Down
3 changes: 2 additions & 1 deletion includes/class-scheduler.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ public static function cleanup_remote_actors() {
\wp_delete_post( $actor->ID );
} elseif ( empty( $meta ) || ! is_array( $meta ) || \is_wp_error( $meta ) ) {
if ( Remote_Actors::count_errors( $actor->ID ) >= 5 ) {
\wp_schedule_single_event( \time(), 'activitypub_delete_actor_interactions', array( $actor->guid ) );
\wp_schedule_single_event( \time(), 'activitypub_delete_remote_actor_interactions', array( $actor->guid ) );
\wp_schedule_single_event( \time(), 'activitypub_delete_remote_actor_posts', array( $actor->guid ) );
\wp_delete_post( $actor->ID );
} else {
Remote_Actors::add_error( $actor->ID, $meta );
Expand Down
54 changes: 54 additions & 0 deletions includes/collection/class-posts.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,33 @@ public static function update( $activity ) {
return \get_post( $post_id );
}

/**
* Delete an object from the collection.
*
* @param int $id The object ID.
*
* @return bool|int|null The deleted post ID, false on failure, or null if no post to delete.
*/
public static function delete( $id ) {
return \wp_delete_post( $id, true );
}

/**
* Delete an object from the collection by its GUID.
*
* @param string $guid The object GUID.
*
* @return bool|int|null The deleted post ID, false on failure, or null if no post to delete.
*/
public static function delete_by_guid( $guid ) {
$post = self::get_by_guid( $guid );
if ( \is_wp_error( $post ) ) {
return $post;
}

return self::delete( $post->ID );
}

/**
* Convert an activity to a post array.
*
Expand Down Expand Up @@ -164,4 +191,31 @@ private static function add_taxonomies( $post_id, $activity_object ) {

\wp_set_post_terms( $post_id, $tags, 'ap_tag' );
}

/**
* Get posts by remote actor.
*
* @param string $actor The remote actor URI.
*
* @return array Array of WP_Post objects.
*/
public static function get_by_remote_actor( $actor ) {
$remote_actor = Remote_Actors::fetch_by_uri( $actor );
if ( \is_wp_error( $remote_actor ) ) {
return array();
}

$query = new \WP_Query(
array(
'post_type' => self::POST_TYPE,
'posts_per_page' => -1,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_key' => '_activitypub_remote_actor_id',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
'meta_value' => $remote_actor->ID,
)
);

return $query->posts;
}
}
134 changes: 115 additions & 19 deletions includes/handler/class-delete.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
namespace Activitypub\Handler;

use Activitypub\Collection\Interactions;
use Activitypub\Collection\Posts;
use Activitypub\Collection\Remote_Actors;
use Activitypub\Tombstone;

use function Activitypub\is_activity_reply;
use function Activitypub\object_to_uri;

/**
Expand All @@ -23,7 +25,8 @@ class Delete {
public static function init() {
\add_action( 'activitypub_inbox_delete', array( self::class, 'handle_delete' ), 10, 2 );
\add_filter( 'activitypub_defer_signature_verification', array( self::class, 'defer_signature_verification' ), 10, 2 );
\add_action( 'activitypub_delete_actor_interactions', array( self::class, 'delete_interactions' ) );
\add_action( 'activitypub_delete_remote_actor_interactions', array( self::class, 'delete_interactions' ) );
\add_action( 'activitypub_delete_remote_actor_posts', array( self::class, 'delete_posts' ) );

\add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ) );
\add_action( 'post_activitypub_add_to_outbox', array( self::class, 'post_add_to_outbox' ), 10, 2 );
Expand All @@ -37,8 +40,6 @@ public static function init() {
*/
public static function handle_delete( $activity, $user_id ) {
$object_type = $activity['object']['type'] ?? '';
$success = false;
$result = null;

switch ( $object_type ) {
/*
Expand All @@ -51,7 +52,7 @@ public static function handle_delete( $activity, $user_id ) {
case 'Organization':
case 'Service':
case 'Application':
$result = self::maybe_delete_follower( $activity );
self::delete_remote_actor( $activity, $user_id );
break;

/*
Expand All @@ -66,7 +67,7 @@ public static function handle_delete( $activity, $user_id ) {
case 'Video':
case 'Event':
case 'Document':
$result = self::maybe_delete_interaction( $activity );
self::delete_object( $activity, $user_id );
break;

/*
Expand All @@ -75,7 +76,7 @@ public static function handle_delete( $activity, $user_id ) {
* @see: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone
*/
case 'Tombstone':
$result = self::maybe_delete_interaction( $activity );
self::delete_object( $activity, $user_id );
break;

/*
Expand All @@ -84,32 +85,65 @@ public static function handle_delete( $activity, $user_id ) {
* @see https://www.w3.org/TR/activitystreams-core/#example-1
*/
default:
// Ignore non Minimal Activities.
if ( ! is_string( $activity['object'] ) ) {
return;
}

// Check if Object is an Actor.
if ( $activity['actor'] === $activity['object'] ) {
$result = self::maybe_delete_follower( $activity );
} else { // Assume an interaction otherwise.
$result = self::maybe_delete_interaction( $activity );
if ( object_to_uri( $activity['object'] ) === $activity['actor'] ) {
self::delete_remote_actor( $activity, $user_id );
} else { // Assume an object otherwise.
self::delete_object( $activity, $user_id );
}
// Maybe handle Delete Activity for other Object Types.
break;
}
}

/**
* Delete an Object.
*
* @param array $activity The Activity object.
* @param int $user_id The user ID.
*/
public static function delete_object( $activity, $user_id ) {
// Check for private and/or direct messages.
if ( is_activity_reply( $activity ) ) {
$result = self::maybe_delete_interaction( $activity );
} else {
$result = self::maybe_delete_post( $activity );
}

$success = ( $result && ! \is_wp_error( $result ) );

/**
* Fires after an ActivityPub Delete activity has been handled.
*
* @param array $activity The ActivityPub activity data.
* @param int $user_id The local user ID.
* @param bool $success True on success, false otherwise.
* @param mixed|null $result The result of the delete operation.
*/
\do_action( 'activitypub_handled_delete', $activity, $user_id, $success, $result );
}

$success = (bool) $result;
/**
* Delete an Actor.
*
* @param array $activity The Activity object.
* @param int $user_id The user ID.
*/
public static function delete_remote_actor( $activity, $user_id ) {
$result = self::maybe_delete_follower( $activity );
$success = ( $result && ! \is_wp_error( $result ) );

/**
* Fires after an ActivityPub Delete activity has been handled.
*
* @param array $activity The ActivityPub activity data.
* @param int $user_id The local user ID.
* @param bool $success True on success, false otherwise.
* @param mixed|null $result The result of the delete operation (e.g., WP_Comment object or deletion status).
* @param mixed|null $result The result of the delete operation.
*/
\do_action( 'activitypub_handled_delete', $activity, $user_id, $success, $result );

return $result;
}

/**
Expand All @@ -126,6 +160,7 @@ public static function maybe_delete_follower( $activity ) {
if ( ! is_wp_error( $follower ) && Tombstone::exists( $activity['actor'] ) ) {
$state = Remote_Actors::delete( $follower->ID );
self::maybe_delete_interactions( $activity );
self::maybe_delete_posts( $activity );
}

return $state ?? false;
Expand All @@ -143,7 +178,29 @@ public static function maybe_delete_interactions( $activity ) {
if ( Tombstone::exists( $activity['actor'] ) ) {
\wp_schedule_single_event(
\time(),
'activitypub_delete_actor_interactions',
'activitypub_delete_remote_actor_interactions',
array( $activity['actor'] )
);

return true;
}

return false;
}

/**
* Delete Reactions if Actor-URL is a Tombstone.
*
* @param array $activity The delete activity.
*
* @return bool True on success, false otherwise.
*/
public static function maybe_delete_posts( $activity ) {
// Verify that Actor is deleted.
if ( Tombstone::exists( $activity['actor'] ) ) {
\wp_schedule_single_event(
\time(),
'activitypub_delete_remote_actor_posts',
array( $activity['actor'] )
);

Expand All @@ -164,7 +221,7 @@ public static function delete_interactions( $actor ) {
$comments = Interactions::get_by_actor( $actor );

foreach ( $comments as $comment ) {
wp_delete_comment( $comment, true );
\wp_delete_comment( $comment, true );
}

if ( $comments ) {
Expand All @@ -174,6 +231,27 @@ public static function delete_interactions( $actor ) {
}
}

/**
* Delete comments from an Actor.
*
* @param string $actor The URL of the actor whose comments to delete.
*
* @return bool True on success, false otherwise.
*/
public static function delete_posts( $actor ) {
$posts = Posts::get_by_remote_actor( $actor );

foreach ( $posts as $post ) {
Posts::delete( $post->ID );
}

if ( $posts ) {
return true;
} else {
return false;
}
}

/**
* Delete a Reaction if URL is a Tombstone.
*
Expand Down Expand Up @@ -201,6 +279,24 @@ public static function maybe_delete_interaction( $activity ) {
return false;
}

/**
* Delete a post from the Posts collection.
*
* @param array $activity The delete activity.
*
* @return bool|\WP_Error True on success, false or WP_Error on failure.
*/
public static function maybe_delete_post( $activity ) {
$id = object_to_uri( $activity['object'] );

// Check if the object exists and is a tombstone.
if ( Tombstone::exists( $id ) ) {
return Posts::delete_by_guid( $id );
}

return false;
}

/**
* Defer signature verification for `Delete` requests.
*
Expand Down
4 changes: 2 additions & 2 deletions tests/phpunit/tests/includes/class-test-scheduler.php
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,7 @@ function () {
\add_filter(
'schedule_event',
function ( $event ) use ( &$scheduled_events ) {
if ( 'activitypub_delete_actor_interactions' === $event->hook ) {
if ( 'activitypub_delete_remote_actor_interactions' === $event->hook ) {
$scheduled_events[] = array(
'hook' => $event->hook,
'args' => $event->args,
Expand All @@ -722,7 +722,7 @@ function () {

// Verify that the event was scheduled with the actor URL as parameter.
$this->assertCount( 1, $scheduled_events, 'Should schedule 1 event' );
$this->assertEquals( 'activitypub_delete_actor_interactions', $scheduled_events[0]['hook'], 'Should schedule the correct hook' );
$this->assertEquals( 'activitypub_delete_remote_actor_interactions', $scheduled_events[0]['hook'], 'Should schedule the correct hook' );
$this->assertCount( 1, $scheduled_events[0]['args'], 'Should have 1 argument' );
$this->assertEquals( 'https://example.com/users/test', $scheduled_events[0]['args'][0], 'Should pass actor URL as parameter' );

Expand Down
Loading