Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3c068c9
Refactor tombstone handling into Tombstone class
pfefferle Aug 8, 2025
7193a64
Refactor Tombstone type check logic
pfefferle Aug 8, 2025
48f196f
Add tests for new Tombstone check methods
pfefferle Aug 8, 2025
f2417fa
Merge branch 'trunk' into add/tombstone
pfefferle Aug 8, 2025
37ae91c
Add changelog
matticbot Aug 8, 2025
7ea9925
Apply suggestion from @Copilot
pfefferle Aug 8, 2025
f6722aa
Apply suggestion from @Copilot
pfefferle Aug 8, 2025
3831be1
Apply suggestion from @Copilot
pfefferle Aug 8, 2025
c8b85ee
Apply suggestion from @Copilot
pfefferle Aug 8, 2025
b550579
Update WP_Error type hint in docblock
obenland Aug 19, 2025
2ed8bc5
Merge branch 'trunk' into add/tombstone
pfefferle Aug 24, 2025
56c8d14
Update templates/tombstone-json.php
pfefferle Aug 24, 2025
823c8ca
Update templates/tombstone-json.php
pfefferle Aug 24, 2025
05315c1
change to `is_gone`
pfefferle Aug 24, 2025
57718e4
Check for `get_error_data`
pfefferle Aug 24, 2025
097e649
Merge branch 'trunk' into add/tombstone
pfefferle Aug 25, 2025
546cfab
Send `Delete` Activity on User-Delete (#2035)
pfefferle Aug 25, 2025
e328903
Merge branch 'trunk' into add/tombstone
pfefferle Aug 25, 2025
28804b2
Remove redundant error code check in Tombstone class
pfefferle Aug 26, 2025
1d00beb
Refactor Tombstone API to use 'exists' naming
pfefferle Aug 26, 2025
576cf22
Update deprecated function reference in is_tombstone
pfefferle Aug 26, 2025
04273e4
Improve and expand docblocks in Tombstone class
pfefferle Aug 26, 2025
82e70ca
Merge branch 'trunk' into add/tombstone
pfefferle Aug 26, 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/2066-from-description
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Improved handling of deleted content with a new unified system for better tracking and compatibility.
5 changes: 5 additions & 0 deletions includes/class-activitypub.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ public static function render_activitypub_template( $template ) {
return $template;
}

if ( Tombstone::check_local_url( Query::get_instance()->get_request_url() ) ) {
\status_header( 410 );
return ACTIVITYPUB_PLUGIN_DIR . 'templates/tombstone-json.php';
}

$activitypub_template = false;
$activitypub_object = Query::get_instance()->get_activitypub_object();

Expand Down
22 changes: 2 additions & 20 deletions includes/class-http.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,27 +182,9 @@ public static function get( $url, $cached = false ) {
* @return bool True if the URL is a tombstone.
*/
public static function is_tombstone( $url ) {
/**
* Fires before checking if the URL is a tombstone.
*
* @param string $url The URL to check.
*/
\do_action( 'activitypub_pre_http_is_tombstone', $url );

$response = \wp_safe_remote_get( $url, array( 'headers' => array( 'Accept' => 'application/activity+json' ) ) );
$code = \wp_remote_retrieve_response_code( $response );

if ( in_array( (int) $code, array( 404, 410 ), true ) ) {
return true;
}

$data = \wp_remote_retrieve_body( $response );
$data = \json_decode( $data, true );
if ( $data && isset( $data['type'] ) && 'Tombstone' === $data['type'] ) {
return true;
}
_deprecated_function( __METHOD__, 'unreleased', 'Activitypub\Tombstone::check_remote_url' );

return false;
return Tombstone::check_remote_url( $url );
}

/**
Expand Down
2 changes: 1 addition & 1 deletion includes/class-query.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ protected function maybe_get_virtual_object() {
*
* @return string|null The request URL.
*/
protected function get_request_url() {
public function get_request_url() {
if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
return null;
}
Expand Down
2 changes: 1 addition & 1 deletion includes/class-scheduler.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ public static function cleanup_remote_actors() {
foreach ( $actors as $actor ) {
$meta = get_remote_metadata_by_actor( $actor->guid, false );

if ( is_tombstone( $meta ) ) {
if ( Tombstone::check( $meta ) ) {
\wp_delete_post( $actor->ID );
} elseif ( empty( $meta ) || ! is_array( $meta ) || \is_wp_error( $meta ) ) {
if ( Actors::count_errors( $actor->ID ) >= 5 ) {
Expand Down
167 changes: 167 additions & 0 deletions includes/class-tombstone.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php
/**
* Tombstone class file.
*
* @package Activitypub
*/

namespace Activitypub;

use Activitypub\Activity\Base_Object;

/**
* ActivityPub Tombstone Class.
*/
class Tombstone {
/**
* HTTP codes that indicate a tombstone.
*
* @var array
*/
private static $codes = array( 404, 410 );

/**
* Check for Tombstone.
*
* @param string|\WP_Error|array|object $various The various data to check.
*
* @return bool True if the various data is a tombstone.
*/
public static function check( $various ) {
if ( \is_wp_error( $various ) ) {
return self::check_wp_error( $various );
}

if ( \is_string( $various ) ) {
if ( is_same_domain( $various ) ) {
return self::check_local_url( $various );
}
return self::check_remote_url( $various );
}

if ( \is_array( $various ) ) {
return self::check_array( $various );
}

if ( \is_object( $various ) ) {
return self::check_object( $various );
}

return false;
}

/**
* Check for remote URL for Tombstone.
*
* @param string $url The URL to check.
*
* @return bool True if the URL is a tombstone.
*/
public static function check_remote_url( $url ) {
/**
* Fires before checking if the URL is a tombstone.
*
* @param string $url The URL to check.
*/
\do_action( 'activitypub_pre_http_is_tombstone', $url );

$response = \wp_safe_remote_get( $url, array( 'headers' => array( 'Accept' => 'application/activity+json' ) ) );
$code = \wp_remote_retrieve_response_code( $response );

if ( in_array( (int) $code, self::$codes, true ) ) {
return true;
}

$data = \wp_remote_retrieve_body( $response );
$data = \json_decode( $data, true );

return self::check_array( $data );
}

/**
* Check for local URL for Tombstone.
*
* @param string $url The URL to check.
*
* @return bool True if the URL is a tombstone.
*/
public static function check_local_url( $url ) {
$urls = get_option( 'activitypub_tombstone_urls', array() );

return in_array( normalize_url( $url ), $urls, true );
}

/**
* Check if the response is a WP_Error.
*
* @param WP_Error $wp_error The response to check.
*
* @return bool True if the response is a WP_Error, false otherwise.
*/
public static function check_wp_error( $wp_error ) {
if ( ! \is_wp_error( $wp_error ) ) {
return false;
}

if ( in_array( (int) $wp_error->get_error_code(), self::$codes, true ) ) {
return true;
}

return false;
}

/**
* Check if the given array represents a tombstone.
*
* @param array $data The array to check.
*
* @return bool True if the array represents a tombstone, false otherwise.
*/
public static function check_array( $data ) {
if ( ! \is_array( $data ) ) {
return false;
}

if ( isset( $data['type'] ) && 'Tombstone' === $data['type'] ) {
return true;
}

return false;
}

/**
* Check if the given object represents a tombstone.
*
* @param object $data The object to check.
*
* @return bool True if the object represents a tombstone, false otherwise.
*/
public static function check_object( $data ) {
if ( ! \is_object( $data ) ) {
return false;
}

if ( isset( $data->type ) && 'Tombstone' === $data->type ) {
return true;
}

if ( $data instanceof Base_Object && 'Tombstone' === $data->get_type() ) {
return true;
}

return false;
}

/**
* Bury a URL.
*
* @param string $url The URL to bury.
*/
public static function bury( $url ) {
$urls = \get_option( 'activitypub_tombstone_urls', array() );
$urls[] = normalize_url( $url );
$urls = \array_unique( $urls );

\update_option( 'activitypub_tombstone_urls', $urls );
}
}
5 changes: 3 additions & 2 deletions includes/collection/class-followers.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

namespace Activitypub\Collection;

use function Activitypub\is_tombstone;
use Activitypub\Tombstone;

use function Activitypub\get_remote_metadata_by_actor;

/**
Expand Down Expand Up @@ -42,7 +43,7 @@ class Followers {
public static function add_follower( $user_id, $actor ) {
$meta = get_remote_metadata_by_actor( $actor );

if ( is_tombstone( $meta ) ) {
if ( Tombstone::check( $meta ) ) {
return $meta;
}

Expand Down
10 changes: 2 additions & 8 deletions includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,15 +204,9 @@ function is_comment() {
* @return boolean True if HTTP-Code is 410 or 404.
*/
function is_tombstone( $wp_error ) {
if ( ! is_wp_error( $wp_error ) ) {
return false;
}

if ( in_array( (int) $wp_error->get_error_code(), array( 404, 410 ), true ) ) {
return true;
}
_deprecated_function( __FUNCTION__, 'unreleased', 'Activitypub\Tombstone::check_wp_error' );

return false;
return Tombstone::check_wp_error( $wp_error );
}

/**
Expand Down
8 changes: 4 additions & 4 deletions includes/handler/class-delete.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace Activitypub\Handler;

use Activitypub\Http;
use Activitypub\Tombstone;
use Activitypub\Collection\Actors;
use Activitypub\Collection\Interactions;

Expand Down Expand Up @@ -104,7 +104,7 @@ public static function maybe_delete_follower( $activity ) {
$follower = Actors::get_remote_by_uri( $activity['actor'] );

// Verify that Actor is deleted.
if ( ! is_wp_error( $follower ) && Http::is_tombstone( $activity['actor'] ) ) {
if ( ! is_wp_error( $follower ) && Tombstone::check( $activity['actor'] ) ) {
Actors::delete( $follower->ID );
self::maybe_delete_interactions( $activity );
}
Expand All @@ -117,7 +117,7 @@ public static function maybe_delete_follower( $activity ) {
*/
public static function maybe_delete_interactions( $activity ) {
// Verify that Actor is deleted.
if ( Http::is_tombstone( $activity['actor'] ) ) {
if ( Tombstone::check( $activity['actor'] ) ) {
\wp_schedule_single_event(
\time(),
'activitypub_delete_actor_interactions',
Expand Down Expand Up @@ -153,7 +153,7 @@ public static function maybe_delete_interaction( $activity ) {

$comments = Interactions::get_interaction_by_id( $id );

if ( $comments && Http::is_tombstone( $id ) ) {
if ( $comments && Tombstone::check( $id ) ) {
foreach ( $comments as $comment ) {
wp_delete_comment( $comment->comment_ID, true );
}
Expand Down
27 changes: 27 additions & 0 deletions templates/tombstone-json.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
/**
* Tombstone JSON template.
*
* @package Activitypub
*/

$object = new \Activitypub\Activity\Base_Object();
$object->set_id( \Activitypub\Query::get_instance()->get_request_url() );
$object->set_type( 'Tombstone' );

/**
* Fires before an ActivityPub object is generated and sent to the client.
*
* @param object $object The ActivityPub object.
*/
\do_action( 'activitypub_json_pre', $object );

\header( 'Content-Type: application/activity+json' );
echo $object->to_json(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped

/**
* Fires after an ActivityPub object is generated and sent to the client.
*
* @param object $object The ActivityPub object.
*/
\do_action( 'activitypub_json_post', $object );
Loading
Loading