From 3c068c98de1ffb59064b169675926819000b623b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 8 Aug 2025 11:56:27 +0200 Subject: [PATCH 01/18] Refactor tombstone handling into Tombstone class Moved all tombstone detection logic into a new Activitypub\Tombstone class, deprecating is_tombstone() and Http::is_tombstone(). Updated all usages to use Tombstone::check() and related methods. Added a tombstone JSON template and updated tests to reflect the new class. This centralizes and standardizes tombstone checks across the codebase. --- includes/class-activitypub.php | 5 + includes/class-http.php | 22 +-- includes/class-query.php | 2 +- includes/class-scheduler.php | 2 +- includes/class-tombstone.php | 170 ++++++++++++++++++ includes/collection/class-followers.php | 5 +- includes/functions.php | 10 +- includes/handler/class-delete.php | 8 +- templates/tombstone-json.php | 27 +++ ...test-http.php => class-test-tombstone.php} | 6 +- 10 files changed, 218 insertions(+), 39 deletions(-) create mode 100644 includes/class-tombstone.php create mode 100644 templates/tombstone-json.php rename tests/includes/{class-test-http.php => class-test-tombstone.php} (91%) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 08deccc99..8711848ce 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -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(); diff --git a/includes/class-http.php b/includes/class-http.php index ecd25ec27..962cacada 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -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 ); } /** diff --git a/includes/class-query.php b/includes/class-query.php index ea5eae870..7690d5e9c 100644 --- a/includes/class-query.php +++ b/includes/class-query.php @@ -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; } diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index b12632c5f..d4bbbbf39 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -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 ) { diff --git a/includes/class-tombstone.php b/includes/class-tombstone.php new file mode 100644 index 000000000..bfabb540c --- /dev/null +++ b/includes/class-tombstone.php @@ -0,0 +1,170 @@ + 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 ); + if ( $data && isset( $data['type'] ) && 'Tombstone' === $data['type'] ) { + return true; + } + + return false; + } + + /** + * 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 response is a WP_Error. + * + * @param array $data The response to check. + * + * @return bool True if the response is a WP_Error, 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 response is a WP_Error. + * + * @param object $data The response to check. + * + * @return bool True if the response is a WP_Error, 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 ); + } +} diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 287587b98..42239614f 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -7,7 +7,8 @@ namespace Activitypub\Collection; -use function Activitypub\is_tombstone; +use Activitypub\Tombstone; + use function Activitypub\get_remote_metadata_by_actor; /** @@ -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; } diff --git a/includes/functions.php b/includes/functions.php index fa8319576..18c67930c 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -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 ); } /** diff --git a/includes/handler/class-delete.php b/includes/handler/class-delete.php index 8f04d5cd1..fb887fd8c 100644 --- a/includes/handler/class-delete.php +++ b/includes/handler/class-delete.php @@ -7,7 +7,7 @@ namespace Activitypub\Handler; -use Activitypub\Http; +use Activitypub\Tombstone; use Activitypub\Collection\Actors; use Activitypub\Collection\Interactions; @@ -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 ); } @@ -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', @@ -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 ); } diff --git a/templates/tombstone-json.php b/templates/tombstone-json.php new file mode 100644 index 000000000..85944b08c --- /dev/null +++ b/templates/tombstone-json.php @@ -0,0 +1,27 @@ +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 ); diff --git a/tests/includes/class-test-http.php b/tests/includes/class-test-tombstone.php similarity index 91% rename from tests/includes/class-test-http.php rename to tests/includes/class-test-tombstone.php index 5d3f57b36..1d74afb36 100644 --- a/tests/includes/class-test-http.php +++ b/tests/includes/class-test-tombstone.php @@ -7,14 +7,14 @@ namespace Activitypub\Tests; -use Activitypub\Http; +use Activitypub\Tombstone; /** * Test class for ActivityPub HTTP Class * * @coversDefaultClass \Activitypub\Http */ -class Test_Http extends \WP_UnitTestCase { +class Test_Tombstone extends \WP_UnitTestCase { /** * Response code is 404 -> is_tombstone returns true @@ -31,7 +31,7 @@ public function test_is_tombstone( $request, $result ) { return $request; }; add_filter( 'pre_http_request', $fake_request, 10, 3 ); - $response = Http::is_tombstone( 'https://fake.test/object/123' ); + $response = Tombstone::is_tombstone( 'https://fake.test/object/123' ); $this->assertEquals( $result, $response ); remove_filter( 'pre_http_request', $fake_request, 10 ); } From 7193a64379dc31238febb43af3990553132900cb Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 8 Aug 2025 11:58:49 +0200 Subject: [PATCH 02/18] Refactor Tombstone type check logic Replaces inline array check in the is_tombstone() method with a call to self::check_array() for improved code reuse and maintainability. --- includes/class-tombstone.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/includes/class-tombstone.php b/includes/class-tombstone.php index bfabb540c..0a7a215dd 100644 --- a/includes/class-tombstone.php +++ b/includes/class-tombstone.php @@ -74,11 +74,8 @@ public static function check_remote_url( $url ) { $data = \wp_remote_retrieve_body( $response ); $data = \json_decode( $data, true ); - if ( $data && isset( $data['type'] ) && 'Tombstone' === $data['type'] ) { - return true; - } - return false; + return self::check_array( $data ); } /** From 48f196f208a217f26a41c50b8d04d2cc1d7d82ec Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 8 Aug 2025 12:06:15 +0200 Subject: [PATCH 03/18] Add tests for new Tombstone check methods Expanded the test suite in class-test-tombstone.php to cover Tombstone::check_wp_error, check_array, check_object, and check_local_url methods. Updated existing tests and data providers to reflect method renaming from is_tombstone to check_remote_url. --- tests/includes/class-test-tombstone.php | 71 +++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/tests/includes/class-test-tombstone.php b/tests/includes/class-test-tombstone.php index 1d74afb36..4708594ee 100644 --- a/tests/includes/class-test-tombstone.php +++ b/tests/includes/class-test-tombstone.php @@ -19,19 +19,19 @@ class Test_Tombstone extends \WP_UnitTestCase { /** * Response code is 404 -> is_tombstone returns true * - * @covers ::is_tombstone + * @covers ::check_remote_url * - * @dataProvider data_is_tombstone + * @dataProvider data_check_remote_url * * @param array $request The request array. * @param bool $result The expected result. */ - public function test_is_tombstone( $request, $result ) { + public function test_check_remote_url( $request, $result ) { $fake_request = function () use ( $request ) { return $request; }; add_filter( 'pre_http_request', $fake_request, 10, 3 ); - $response = Tombstone::is_tombstone( 'https://fake.test/object/123' ); + $response = Tombstone::check_remote_url( 'https://fake.test/object/123' ); $this->assertEquals( $result, $response ); remove_filter( 'pre_http_request', $fake_request, 10 ); } @@ -41,7 +41,7 @@ public function test_is_tombstone( $request, $result ) { * * @return array */ - public function data_is_tombstone() { + public function data_check_remote_url() { return array( array( array( 'response' => array( 'code' => 404 ) ), true ), array( array( 'response' => array( 'code' => 410 ) ), true ), @@ -82,4 +82,65 @@ public function data_is_tombstone() { ), ); } + + /** + * Response code is 404 -> is_tombstone returns true + * + * @covers ::check_wp_error + */ + public function test_check_wp_error() { + $response = Tombstone::check_wp_error( new \WP_Error( 404 ) ); + $this->assertTrue( $response ); + + $response = Tombstone::check_wp_error( new \WP_Error( 410 ) ); + $this->assertTrue( $response ); + + $response = Tombstone::check_wp_error( new \WP_Error( 200 ) ); + $this->assertFalse( $response ); + } + + /** + * Response code is 404 -> is_tombstone returns true + * + * @covers ::check_array + */ + public function test_check_array() { + $response = Tombstone::check_array( array( 'type' => 'Tombstone' ) ); + $this->assertTrue( $response ); + + $response = Tombstone::check_array( array( 'type' => 'Note' ) ); + $this->assertFalse( $response ); + } + + /** + * Response code is 404 -> is_tombstone returns true + * + * @covers ::check_object + */ + public function test_check_object() { + $response = Tombstone::check_object( (object) array( 'type' => 'Tombstone' ) ); + $this->assertTrue( $response ); + + $response = Tombstone::check_object( (object) array( 'type' => 'Note' ) ); + $this->assertFalse( $response ); + } + + /** + * Response code is 404 -> is_tombstone returns true + * + * @covers ::check_local_url + */ + public function test_check_local_url() { + $url = 'https://fake.test/object/123'; + + $response = Tombstone::check_local_url( $url ); + $this->assertFalse( $response ); + + Tombstone::bury( $url ); + + $response = Tombstone::check_local_url( $url ); + $this->assertTrue( $response ); + + \delete_option( 'activitypub_tombstone_urls' ); + } } From 37ae91c8b5571c9060a70c0374a32ebce824edec Mon Sep 17 00:00:00 2001 From: Automattic Bot Date: Fri, 8 Aug 2025 20:10:48 +0200 Subject: [PATCH 04/18] Add changelog --- .github/changelog/2066-from-description | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/changelog/2066-from-description diff --git a/.github/changelog/2066-from-description b/.github/changelog/2066-from-description new file mode 100644 index 000000000..344b8c0f6 --- /dev/null +++ b/.github/changelog/2066-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Improved handling of deleted content with a new unified system for better tracking and compatibility. From 7ea9925a9291e4f248af5e2348b9ea9ce9d044ea Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 8 Aug 2025 20:11:58 +0200 Subject: [PATCH 05/18] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- includes/class-tombstone.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/class-tombstone.php b/includes/class-tombstone.php index 0a7a215dd..ba452dba2 100644 --- a/includes/class-tombstone.php +++ b/includes/class-tombstone.php @@ -111,11 +111,11 @@ public static function check_wp_error( $wp_error ) { } /** - * Check if the response is a WP_Error. + * Check if the given array represents a tombstone. * - * @param array $data The response to check. + * @param array $data The array to check. * - * @return bool True if the response is a WP_Error, false otherwise. + * @return bool True if the array represents a tombstone, false otherwise. */ public static function check_array( $data ) { if ( ! \is_array( $data ) ) { From f6722aa523eab498e28e2b1f8fadc32c12121804 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 8 Aug 2025 20:12:13 +0200 Subject: [PATCH 06/18] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- includes/class-tombstone.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/class-tombstone.php b/includes/class-tombstone.php index ba452dba2..1f01d9c9a 100644 --- a/includes/class-tombstone.php +++ b/includes/class-tombstone.php @@ -130,11 +130,11 @@ public static function check_array( $data ) { } /** - * Check if the response is a WP_Error. + * Check if the given object represents a tombstone. * - * @param object $data The response to check. + * @param object $data The object to check. * - * @return bool True if the response is a WP_Error, false otherwise. + * @return bool True if the object represents a tombstone, false otherwise. */ public static function check_object( $data ) { if ( ! \is_object( $data ) ) { From 3831be167eee2a096d6ac68ee90e1c807b5424ea Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 8 Aug 2025 20:12:39 +0200 Subject: [PATCH 07/18] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/includes/class-test-tombstone.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/includes/class-test-tombstone.php b/tests/includes/class-test-tombstone.php index 4708594ee..c54dea211 100644 --- a/tests/includes/class-test-tombstone.php +++ b/tests/includes/class-test-tombstone.php @@ -10,9 +10,9 @@ use Activitypub\Tombstone; /** - * Test class for ActivityPub HTTP Class + * Test class for ActivityPub Tombstone Class * - * @coversDefaultClass \Activitypub\Http + * @coversDefaultClass \Activitypub\Tombstone */ class Test_Tombstone extends \WP_UnitTestCase { From c8b85ee15fd1b43d93d52f8ab4a8c8e7413478e6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 8 Aug 2025 20:12:52 +0200 Subject: [PATCH 08/18] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/includes/class-test-tombstone.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/includes/class-test-tombstone.php b/tests/includes/class-test-tombstone.php index c54dea211..098491824 100644 --- a/tests/includes/class-test-tombstone.php +++ b/tests/includes/class-test-tombstone.php @@ -1,6 +1,6 @@ Date: Tue, 19 Aug 2025 08:36:36 -0500 Subject: [PATCH 09/18] Update WP_Error type hint in docblock Changed the docblock parameter type from WP_Error to \WP_Error for improved clarity and namespacing consistency. --- includes/class-tombstone.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-tombstone.php b/includes/class-tombstone.php index 1f01d9c9a..271c522bb 100644 --- a/includes/class-tombstone.php +++ b/includes/class-tombstone.php @@ -94,7 +94,7 @@ public static function check_local_url( $url ) { /** * Check if the response is a WP_Error. * - * @param WP_Error $wp_error The response to check. + * @param \WP_Error $wp_error The response to check. * * @return bool True if the response is a WP_Error, false otherwise. */ From 56c8d14a05a1c2cd1bff53402c08fa67fe503a83 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sun, 24 Aug 2025 19:32:57 +0200 Subject: [PATCH 10/18] Update templates/tombstone-json.php Co-authored-by: Konstantin Obenland --- templates/tombstone-json.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/tombstone-json.php b/templates/tombstone-json.php index 85944b08c..156ff1948 100644 --- a/templates/tombstone-json.php +++ b/templates/tombstone-json.php @@ -22,6 +22,6 @@ /** * Fires after an ActivityPub object is generated and sent to the client. * - * @param object $object The ActivityPub object. + * @param Activitypub\Activity\Base_Object $object The ActivityPub object. */ \do_action( 'activitypub_json_post', $object ); From 823c8cac82c69554ba12bbeff3d133d1315a11d2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sun, 24 Aug 2025 19:33:35 +0200 Subject: [PATCH 11/18] Update templates/tombstone-json.php Co-authored-by: Konstantin Obenland --- templates/tombstone-json.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/tombstone-json.php b/templates/tombstone-json.php index 156ff1948..0f826de3d 100644 --- a/templates/tombstone-json.php +++ b/templates/tombstone-json.php @@ -12,7 +12,7 @@ /** * Fires before an ActivityPub object is generated and sent to the client. * - * @param object $object The ActivityPub object. + * @param Activitypub\Activity\Base_Object $object The ActivityPub object. */ \do_action( 'activitypub_json_pre', $object ); From 05315c1618c55469d1f47dea83dcf4a72956c0a8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sun, 24 Aug 2025 19:44:57 +0200 Subject: [PATCH 12/18] change to `is_gone` @obenland what about `is_gone`? because the tombstone check is for `410 gone`. --- includes/class-activitypub.php | 2 +- includes/class-http.php | 4 +-- includes/class-scheduler.php | 2 +- includes/class-tombstone.php | 24 +++++++------- includes/collection/class-followers.php | 2 +- includes/functions.php | 4 +-- includes/handler/class-delete.php | 6 ++-- tests/includes/class-test-tombstone.php | 44 ++++++++++++------------- 8 files changed, 44 insertions(+), 44 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index b59a91e76..38eb93a5c 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -117,7 +117,7 @@ public static function render_activitypub_template( $template ) { return $template; } - if ( Tombstone::check_local_url( Query::get_instance()->get_request_url() ) ) { + if ( Tombstone::is_local_url_gone( Query::get_instance()->get_request_url() ) ) { \status_header( 410 ); return ACTIVITYPUB_PLUGIN_DIR . 'templates/tombstone-json.php'; } diff --git a/includes/class-http.php b/includes/class-http.php index 962cacada..fcf7ec1e8 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -182,9 +182,9 @@ public static function get( $url, $cached = false ) { * @return bool True if the URL is a tombstone. */ public static function is_tombstone( $url ) { - _deprecated_function( __METHOD__, 'unreleased', 'Activitypub\Tombstone::check_remote_url' ); + _deprecated_function( __METHOD__, 'unreleased', 'Activitypub\Tombstone::is_remote_url_gone' ); - return Tombstone::check_remote_url( $url ); + return Tombstone::is_remote_url_gone( $url ); } /** diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index d4bbbbf39..7243f1b2f 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -194,7 +194,7 @@ public static function cleanup_remote_actors() { foreach ( $actors as $actor ) { $meta = get_remote_metadata_by_actor( $actor->guid, false ); - if ( Tombstone::check( $meta ) ) { + if ( Tombstone::is_gone( $meta ) ) { \wp_delete_post( $actor->ID ); } elseif ( empty( $meta ) || ! is_array( $meta ) || \is_wp_error( $meta ) ) { if ( Actors::count_errors( $actor->ID ) >= 5 ) { diff --git a/includes/class-tombstone.php b/includes/class-tombstone.php index 271c522bb..93e8a6f5e 100644 --- a/includes/class-tombstone.php +++ b/includes/class-tombstone.php @@ -27,24 +27,24 @@ class Tombstone { * * @return bool True if the various data is a tombstone. */ - public static function check( $various ) { + public static function is_gone( $various ) { if ( \is_wp_error( $various ) ) { - return self::check_wp_error( $various ); + return self::is_wp_error( $various ); } if ( \is_string( $various ) ) { if ( is_same_domain( $various ) ) { - return self::check_local_url( $various ); + return self::is_local_url_gone( $various ); } - return self::check_remote_url( $various ); + return self::is_remote_url_gone( $various ); } if ( \is_array( $various ) ) { - return self::check_array( $various ); + return self::is_array_gone( $various ); } if ( \is_object( $various ) ) { - return self::check_object( $various ); + return self::is_object_gone( $various ); } return false; @@ -57,7 +57,7 @@ public static function check( $various ) { * * @return bool True if the URL is a tombstone. */ - public static function check_remote_url( $url ) { + public static function is_remote_url_gone( $url ) { /** * Fires before checking if the URL is a tombstone. * @@ -75,7 +75,7 @@ public static function check_remote_url( $url ) { $data = \wp_remote_retrieve_body( $response ); $data = \json_decode( $data, true ); - return self::check_array( $data ); + return self::is_array_gone( $data ); } /** @@ -85,7 +85,7 @@ public static function check_remote_url( $url ) { * * @return bool True if the URL is a tombstone. */ - public static function check_local_url( $url ) { + public static function is_local_url_gone( $url ) { $urls = get_option( 'activitypub_tombstone_urls', array() ); return in_array( normalize_url( $url ), $urls, true ); @@ -98,7 +98,7 @@ public static function check_local_url( $url ) { * * @return bool True if the response is a WP_Error, false otherwise. */ - public static function check_wp_error( $wp_error ) { + public static function is_wp_error( $wp_error ) { if ( ! \is_wp_error( $wp_error ) ) { return false; } @@ -117,7 +117,7 @@ public static function check_wp_error( $wp_error ) { * * @return bool True if the array represents a tombstone, false otherwise. */ - public static function check_array( $data ) { + public static function is_array_gone( $data ) { if ( ! \is_array( $data ) ) { return false; } @@ -136,7 +136,7 @@ public static function check_array( $data ) { * * @return bool True if the object represents a tombstone, false otherwise. */ - public static function check_object( $data ) { + public static function is_object_gone( $data ) { if ( ! \is_object( $data ) ) { return false; } diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 42239614f..0e6315f6c 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -43,7 +43,7 @@ class Followers { public static function add_follower( $user_id, $actor ) { $meta = get_remote_metadata_by_actor( $actor ); - if ( Tombstone::check( $meta ) ) { + if ( Tombstone::is_gone( $meta ) ) { return $meta; } diff --git a/includes/functions.php b/includes/functions.php index 18c67930c..bd35420b6 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -204,9 +204,9 @@ function is_comment() { * @return boolean True if HTTP-Code is 410 or 404. */ function is_tombstone( $wp_error ) { - _deprecated_function( __FUNCTION__, 'unreleased', 'Activitypub\Tombstone::check_wp_error' ); + _deprecated_function( __FUNCTION__, 'unreleased', 'Activitypub\Tombstone::is_wp_error' ); - return Tombstone::check_wp_error( $wp_error ); + return Tombstone::is_wp_error( $wp_error ); } /** diff --git a/includes/handler/class-delete.php b/includes/handler/class-delete.php index fb887fd8c..a9306b49e 100644 --- a/includes/handler/class-delete.php +++ b/includes/handler/class-delete.php @@ -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 ) && Tombstone::check( $activity['actor'] ) ) { + if ( ! is_wp_error( $follower ) && Tombstone::is_gone( $activity['actor'] ) ) { Actors::delete( $follower->ID ); self::maybe_delete_interactions( $activity ); } @@ -117,7 +117,7 @@ public static function maybe_delete_follower( $activity ) { */ public static function maybe_delete_interactions( $activity ) { // Verify that Actor is deleted. - if ( Tombstone::check( $activity['actor'] ) ) { + if ( Tombstone::is_gone( $activity['actor'] ) ) { \wp_schedule_single_event( \time(), 'activitypub_delete_actor_interactions', @@ -153,7 +153,7 @@ public static function maybe_delete_interaction( $activity ) { $comments = Interactions::get_interaction_by_id( $id ); - if ( $comments && Tombstone::check( $id ) ) { + if ( $comments && Tombstone::is_gone( $id ) ) { foreach ( $comments as $comment ) { wp_delete_comment( $comment->comment_ID, true ); } diff --git a/tests/includes/class-test-tombstone.php b/tests/includes/class-test-tombstone.php index 098491824..9ca7526fb 100644 --- a/tests/includes/class-test-tombstone.php +++ b/tests/includes/class-test-tombstone.php @@ -19,19 +19,19 @@ class Test_Tombstone extends \WP_UnitTestCase { /** * Response code is 404 -> is_tombstone returns true * - * @covers ::check_remote_url + * @covers ::is_remote_url_gone * - * @dataProvider data_check_remote_url + * @dataProvider data_is_remote_url_gone * * @param array $request The request array. * @param bool $result The expected result. */ - public function test_check_remote_url( $request, $result ) { + public function test_is_remote_url_gone( $request, $result ) { $fake_request = function () use ( $request ) { return $request; }; add_filter( 'pre_http_request', $fake_request, 10, 3 ); - $response = Tombstone::check_remote_url( 'https://fake.test/object/123' ); + $response = Tombstone::is_remote_url_gone( 'https://fake.test/object/123' ); $this->assertEquals( $result, $response ); remove_filter( 'pre_http_request', $fake_request, 10 ); } @@ -41,7 +41,7 @@ public function test_check_remote_url( $request, $result ) { * * @return array */ - public function data_check_remote_url() { + public function data_is_remote_url_gone() { return array( array( array( 'response' => array( 'code' => 404 ) ), true ), array( array( 'response' => array( 'code' => 410 ) ), true ), @@ -86,59 +86,59 @@ public function data_check_remote_url() { /** * Response code is 404 -> is_tombstone returns true * - * @covers ::check_wp_error + * @covers ::is_wp_error */ - public function test_check_wp_error() { - $response = Tombstone::check_wp_error( new \WP_Error( 404 ) ); + public function test_is_wp_error() { + $response = Tombstone::is_wp_error( new \WP_Error( 404 ) ); $this->assertTrue( $response ); - $response = Tombstone::check_wp_error( new \WP_Error( 410 ) ); + $response = Tombstone::is_wp_error( new \WP_Error( 410 ) ); $this->assertTrue( $response ); - $response = Tombstone::check_wp_error( new \WP_Error( 200 ) ); + $response = Tombstone::is_wp_error( new \WP_Error( 200 ) ); $this->assertFalse( $response ); } /** * Response code is 404 -> is_tombstone returns true * - * @covers ::check_array + * @covers ::is_array_gone */ - public function test_check_array() { - $response = Tombstone::check_array( array( 'type' => 'Tombstone' ) ); + public function test_is_array_gone() { + $response = Tombstone::is_array_gone( array( 'type' => 'Tombstone' ) ); $this->assertTrue( $response ); - $response = Tombstone::check_array( array( 'type' => 'Note' ) ); + $response = Tombstone::is_array_gone( array( 'type' => 'Note' ) ); $this->assertFalse( $response ); } /** * Response code is 404 -> is_tombstone returns true * - * @covers ::check_object + * @covers ::is_object_gone */ - public function test_check_object() { - $response = Tombstone::check_object( (object) array( 'type' => 'Tombstone' ) ); + public function test_is_object_gone() { + $response = Tombstone::is_object_gone( (object) array( 'type' => 'Tombstone' ) ); $this->assertTrue( $response ); - $response = Tombstone::check_object( (object) array( 'type' => 'Note' ) ); + $response = Tombstone::is_object_gone( (object) array( 'type' => 'Note' ) ); $this->assertFalse( $response ); } /** * Response code is 404 -> is_tombstone returns true * - * @covers ::check_local_url + * @covers ::is_local_url_gone */ - public function test_check_local_url() { + public function test_is_local_url_gone() { $url = 'https://fake.test/object/123'; - $response = Tombstone::check_local_url( $url ); + $response = Tombstone::is_local_url_gone( $url ); $this->assertFalse( $response ); Tombstone::bury( $url ); - $response = Tombstone::check_local_url( $url ); + $response = Tombstone::is_local_url_gone( $url ); $this->assertTrue( $response ); \delete_option( 'activitypub_tombstone_urls' ); From 57718e49be85e7ec932df08c645c3ae7b1c36fce Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sun, 24 Aug 2025 19:48:14 +0200 Subject: [PATCH 13/18] Check for `get_error_data` --- includes/class-tombstone.php | 5 +++++ tests/includes/class-test-tombstone.php | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/includes/class-tombstone.php b/includes/class-tombstone.php index 93e8a6f5e..06a5a7ee6 100644 --- a/includes/class-tombstone.php +++ b/includes/class-tombstone.php @@ -107,6 +107,11 @@ public static function is_wp_error( $wp_error ) { return true; } + $data = $wp_error->get_error_data(); + if ( isset( $data['status'] ) && in_array( (int) $data['status'], self::$codes, true ) ) { + return true; + } + return false; } diff --git a/tests/includes/class-test-tombstone.php b/tests/includes/class-test-tombstone.php index 9ca7526fb..0e978a714 100644 --- a/tests/includes/class-test-tombstone.php +++ b/tests/includes/class-test-tombstone.php @@ -97,6 +97,15 @@ public function test_is_wp_error() { $response = Tombstone::is_wp_error( new \WP_Error( 200 ) ); $this->assertFalse( $response ); + + $response = Tombstone::is_wp_error( new \WP_Error( 'foo', '', array( 'status' => 404 ) ) ); + $this->assertTrue( $response ); + + $response = Tombstone::is_wp_error( new \WP_Error( 'bar', '', array( 'status' => 410 ) ) ); + $this->assertTrue( $response ); + + $response = Tombstone::is_wp_error( new \WP_Error( 'baz', '', array( 'status' => 200 ) ) ); + $this->assertFalse( $response ); } /** From 546cfabb7e73c246ea07d980e884e4487a078abf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 25 Aug 2025 23:09:22 +0200 Subject: [PATCH 14/18] Send `Delete` Activity on User-Delete (#2035) --- .github/changelog/2035-from-description | 4 + includes/class-dispatcher.php | 6 +- includes/class-http.php | 2 +- includes/collection/class-actors.php | 33 +++++++ includes/collection/class-followers.php | 21 ++-- includes/collection/class-outbox.php | 13 ++- includes/scheduler/class-actor.php | 49 ++++++++++ .../collection/class-test-followers.php | 7 +- tests/includes/scheduler/class-test-actor.php | 96 +++++++++++++++++++ 9 files changed, 213 insertions(+), 18 deletions(-) create mode 100644 .github/changelog/2035-from-description diff --git a/.github/changelog/2035-from-description b/.github/changelog/2035-from-description new file mode 100644 index 000000000..a09d5c4a7 --- /dev/null +++ b/.github/changelog/2035-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Adds support for sending Delete activities when a user is removed. diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index d5c37b59d..5b4686de6 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -60,8 +60,9 @@ public static function process_outbox( $id ) { return; } + $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); $actor = Outbox::get_actor( $outbox_item ); - if ( \is_wp_error( $actor ) ) { + if ( \is_wp_error( $actor ) && 'Delete' !== $type ) { // If the actor is not found, publish the post and don't try again. \wp_publish_post( $outbox_item ); return; @@ -99,8 +100,7 @@ public static function send_to_followers( $outbox_item_id, $batch_size = ACTIVIT $outbox_item = \get_post( $outbox_item_id ); $json = Outbox::get_activity( $outbox_item_id )->to_json(); $inboxes = Followers::get_inboxes_for_activity( $json, $outbox_item->post_author, $batch_size, $offset ); - - $retries = self::send_to_inboxes( $inboxes, $outbox_item_id ); + $retries = self::send_to_inboxes( $inboxes, $outbox_item_id ); // Retry failed inboxes. if ( ! empty( $retries ) ) { diff --git a/includes/class-http.php b/includes/class-http.php index fcf7ec1e8..ba23e97fa 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -52,7 +52,7 @@ public static function post( $url, $body, $user_id ) { 'Date' => \gmdate( 'D, d M Y H:i:s T' ), ), 'body' => $body, - 'key_id' => Actors::get_by_id( $user_id )->get_id() . '#main-key', + 'key_id' => \json_decode( $body )->actor . '#main-key', 'private_key' => Actors::get_private_key( $user_id ), ); diff --git a/includes/collection/class-actors.php b/includes/collection/class-actors.php index af78ae99f..7147692a1 100644 --- a/includes/collection/class-actors.php +++ b/includes/collection/class-actors.php @@ -49,6 +49,13 @@ class Actors { */ const POST_TYPE = 'ap_actor'; + /** + * Cache key for the followers inbox. + * + * @var string + */ + const CACHE_KEY_INBOXES = 'actor_inboxes'; + /** * Get the Actor by ID. * @@ -421,6 +428,32 @@ public static function get_all() { return $return; } + /** + * Returns all Inboxes for all known remote Actors. + * + * @return array The list of Inboxes. + */ + public static function get_inboxes() { + $inboxes = \wp_cache_get( self::CACHE_KEY_INBOXES, 'activitypub' ); + + if ( $inboxes ) { + return $inboxes; + } + + global $wpdb; + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + $results = $wpdb->get_col( + "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} + WHERE meta_key = '_activitypub_inbox' + AND meta_value IS NOT NULL" + ); + + $inboxes = \array_filter( $results ); + \wp_cache_set( self::CACHE_KEY_INBOXES, $inboxes, 'activitypub' ); + + return $inboxes; + } + /** * Returns the actor type based on the user ID. * diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 0e6315f6c..944eb81a9 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -60,6 +60,7 @@ public static function add_follower( $user_id, $actor ) { if ( \is_array( $post_meta ) && ! \in_array( (string) $user_id, $post_meta, true ) ) { \add_post_meta( $post_id, self::FOLLOWER_META_KEY, $user_id ); \wp_cache_delete( \sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); + \wp_cache_delete( Actors::CACHE_KEY_INBOXES, 'activitypub' ); } return $post_id; @@ -81,6 +82,7 @@ public static function remove( $post_id, $user_id ) { } \wp_cache_delete( \sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); + \wp_cache_delete( Actors::CACHE_KEY_INBOXES, 'activitypub' ); /** * Fires before a Follower is removed. @@ -310,14 +312,12 @@ public static function get_inboxes( $user_id ) { * @return array The list of Inboxes. */ public static function get_inboxes_for_activity( $json, $actor_id, $batch_size = 50, $offset = 0 ) { - $inboxes = self::get_inboxes( $actor_id ); - - if ( self::maybe_add_inboxes_of_blog_user( $json, $actor_id ) ) { - $inboxes = \array_fill_keys( $inboxes, 1 ); - foreach ( self::get_inboxes( Actors::BLOG_USER_ID ) as $inbox ) { - $inboxes[ $inbox ] = 1; - } - $inboxes = \array_keys( $inboxes ); + $activity = \json_decode( $json, true ); + // Only if this is a Delete. Create handles its own "Announce" in dual user mode. + if ( 'Delete' === ( $activity['type'] ?? null ) ) { + $inboxes = Actors::get_inboxes(); + } else { + $inboxes = self::get_inboxes( $actor_id ); } return \array_slice( $inboxes, $offset, $batch_size ); @@ -326,11 +326,16 @@ public static function get_inboxes_for_activity( $json, $actor_id, $batch_size = /** * Maybe add Inboxes of the Blog User. * + * @deprecated unreleased + * * @param string $json The ActivityPub Activity JSON. * @param int $actor_id The WordPress Actor ID. + * * @return bool True if the Inboxes of the Blog User should be added, false otherwise. */ public static function maybe_add_inboxes_of_blog_user( $json, $actor_id ) { + \_deprecated_function( __METHOD__, 'unreleased' ); + // Only if we're in both Blog and User modes. if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { return false; diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 6c575e882..e83e44587 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -242,10 +242,6 @@ public static function reschedule( $outbox_item ) { */ public static function get_activity( $outbox_item ) { $outbox_item = \get_post( $outbox_item ); - $actor = self::get_actor( $outbox_item ); - if ( is_wp_error( $actor ) ) { - return $actor; - } $activity_object = \json_decode( $outbox_item->post_content, true ); $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); @@ -253,9 +249,18 @@ public static function get_activity( $outbox_item ) { if ( $activity_object['type'] === $type ) { $activity = Activity::init_from_array( $activity_object ); if ( ! $activity->get_actor() ) { + $actor = self::get_actor( $outbox_item ); + if ( \is_wp_error( $actor ) ) { + return $actor; + } $activity->set_actor( $actor->get_id() ); } } else { + $actor = self::get_actor( $outbox_item ); + if ( \is_wp_error( $actor ) ) { + return $actor; + } + $activity = new Activity(); $activity->set_type( $type ); $activity->set_id( $outbox_item->guid ); diff --git a/includes/scheduler/class-actor.php b/includes/scheduler/class-actor.php index 58e670c46..6fa09272f 100644 --- a/includes/scheduler/class-actor.php +++ b/includes/scheduler/class-actor.php @@ -7,8 +7,11 @@ namespace Activitypub\Scheduler; +use Activitypub\Activity\Activity; use Activitypub\Collection\Actors; use Activitypub\Collection\Extra_Fields; +use Activitypub\Collection\Outbox; +use Activitypub\Tombstone; use function Activitypub\add_to_outbox; use function Activitypub\is_user_type_disabled; @@ -51,6 +54,10 @@ public static function init() { \add_action( 'post_stuck', array( self::class, 'sticky_post_update' ) ); \add_action( 'post_unstuck', array( self::class, 'sticky_post_update' ) ); + + // User deletion handling. + \add_action( 'delete_user', array( self::class, 'schedule_user_delete' ), 10, 3 ); + \add_filter( 'post_types_to_delete_with_user', array( self::class, 'post_types_to_delete_with_user' ) ); } /** @@ -161,4 +168,46 @@ public static function sticky_post_update( $post_id ) { self::schedule_profile_update( $post->post_author ); } + + /** + * Schedule a Delete activity when a user is deleted. + * + * @param int $user_id The user ID being deleted. + */ + public static function schedule_user_delete( $user_id ) { + // Don't bother if the user can't publish ActivityPub content. + if ( ! \user_can( $user_id, 'activitypub' ) ) { + return; + } + + // Get the actor before deletion to ensure we have the data. + $actor = Actors::get_by_id( $user_id ); + if ( \is_wp_error( $actor ) ) { + return; + } + + Tombstone::bury( $actor->get_id() ); + Tombstone::bury( $actor->get_url() ); + + $activity = new Activity(); + $activity->set_actor( $actor->get_id() ); + $activity->set_object( $actor->get_id() ); + $activity->set_type( 'Delete' ); + + add_to_outbox( $activity, null, $user_id ); + } + + /** + * Remove outbox from post types to delete with user. + * + * Outbox items should not be deleted with the user, because we + * need to federate the `Delete` Activities. + * + * @param array $post_types The post types to delete with user. + * + * @return array The post types to delete with user without outbox. + */ + public static function post_types_to_delete_with_user( $post_types ) { + return \array_diff( $post_types, array( Outbox::POST_TYPE ) ); + } } diff --git a/tests/includes/collection/class-test-followers.php b/tests/includes/collection/class-test-followers.php index a7887ac4e..1a4ffc935 100644 --- a/tests/includes/collection/class-test-followers.php +++ b/tests/includes/collection/class-test-followers.php @@ -479,6 +479,7 @@ public function test_get_inboxes() { $this->assertCount( 30, $inboxes ); wp_cache_delete( sprintf( Followers::CACHE_KEY_INBOXES, 1 ), 'activitypub' ); + wp_cache_delete( Actors::CACHE_KEY_INBOXES, 'activitypub' ); for ( $j = 0; $j < 5; $j++ ) { $k = $j + 100; @@ -589,6 +590,8 @@ public function data_maybe_add_inboxes_of_blog_user() { * @covers ::maybe_add_inboxes_of_blog_user * @dataProvider data_maybe_add_inboxes_of_blog_user * + * @expectedDeprecated Activitypub\Collection\Followers::maybe_add_inboxes_of_blog_user + * * @param string $actor_mode The actor mode to test with. * @param string $json The JSON to test with. * @param int $actor_id The actor ID to test with. @@ -632,7 +635,7 @@ public function test_get_inboxes_for_activity() { ); // username and jon have sharedInbox endpoints. - $this->assertCount( 2, $inboxes, 'Should retrieve exactly 3 inboxes.' ); + $this->assertCount( 2, $inboxes, 'Should retrieve exactly 2 inboxes.' ); $this->assertContains( self::$actors['username@example.org']['endpoints']['sharedInbox'], $inboxes, 'Should contain first inbox.' ); $this->assertContains( self::$actors['doe@example.org']['inbox'], $inboxes, 'Should contain second inbox.' ); @@ -650,7 +653,7 @@ public function test_get_inboxes_for_activity() { Followers::add_follower( Actors::BLOG_USER_ID, self::$actors['sally@example.org']['id'] ); $inboxes = Followers::get_inboxes_for_activity( - '{"type":"Update"}', + '{"type":"Delete"}', $actor_id, 50, 0 diff --git a/tests/includes/scheduler/class-test-actor.php b/tests/includes/scheduler/class-test-actor.php index a4f3eee25..051329559 100644 --- a/tests/includes/scheduler/class-test-actor.php +++ b/tests/includes/scheduler/class-test-actor.php @@ -351,4 +351,100 @@ public function test_sticky_post_update() { \wp_delete_post( $post_id ); \wp_delete_user( $user_id ); } + + /** + * Test that user deletion creates a Delete activity. + * + * @covers ::schedule_user_delete + */ + public function test_schedule_user_delete() { + // Create a user with ActivityPub capability. + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + + // Verify the user has ActivityPub capability. + $this->assertTrue( \user_can( $user_id, 'activitypub' ) ); + + // Get the actor before deletion. + $actor = Actors::get_by_id( $user_id ); + $this->assertNotNull( $actor ); + $this->assertFalse( \is_wp_error( $actor ) ); + + // Get the current outbox count. + $outbox_before = $this->get_latest_outbox_item(); + $this->assertNull( $outbox_before ); + + // Call the method directly to test it. + Actor::schedule_user_delete( $user_id ); + + // Check that a Delete activity was added to the outbox. + $outbox_after = $this->get_latest_outbox_item(); + $this->assertNotNull( $outbox_after ); + + // Verify it's a Delete activity. + $activity_type = \get_post_meta( $outbox_after->ID, '_activitypub_activity_type', true ); + $this->assertEquals( 'Delete', $activity_type, 'Activity type should be Delete' ); + + // Verify the activity content. + $activity = \json_decode( $outbox_after->post_content, true ); + $this->assertIsArray( $activity, 'Activity content should be valid JSON' ); + $this->assertEquals( 'Delete', $activity['type'], 'Activity type in content should be Delete' ); + $this->assertEquals( $actor->get_id(), $activity['actor'], 'Actor should match' ); + $this->assertEquals( $actor->get_id(), $activity['object'], 'Object should be the actor being deleted' ); + + // Clean up. + \wp_delete_user( $user_id ); + } + + /** + * Test that user deletion is skipped for users without ActivityPub capability. + * + * @covers ::schedule_user_delete + */ + public function test_schedule_user_delete_skips_non_activitypub_users() { + // Create a user without ActivityPub capability (subscriber role). + $user_id = self::factory()->user->create( array( 'role' => 'subscriber' ) ); + + // Verify the user doesn't have ActivityPub capability. + $this->assertFalse( \user_can( $user_id, 'activitypub' ) ); + + // Get the current total outbox items across all users. + $total_before = \wp_count_posts( 'ap_outbox' )->publish; + + // Call the method directly to test it. + Actor::schedule_user_delete( $user_id ); + + // Check that no Delete activity was added. + $total_after = \wp_count_posts( 'ap_outbox' )->publish; + $this->assertEquals( $total_before, $total_after, 'No Delete activity should be added for non-ActivityPub users' ); + + // Clean up. + \wp_delete_user( $user_id ); + } + + /** + * Test that user deletion handles invalid actor gracefully. + * + * @covers ::schedule_user_delete + */ + public function test_schedule_user_delete_handles_invalid_actor() { + // Create a user with ActivityPub capability. + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + + // Verify the user has ActivityPub capability. + $this->assertTrue( \user_can( $user_id, 'activitypub' ) ); + + // We'll use a filter to mock the response instead since we can't easily mock static methods. + // For this test, we'll delete the user first to make the actor invalid. + \wp_delete_user( $user_id ); + + // Get the current total outbox items. + $total_before = \wp_count_posts( 'ap_outbox' )->publish; + + // Call the method with the deleted user ID. + Actor::schedule_user_delete( $user_id ); + + // Check that no Delete activity was added since the actor is invalid. + $total_after = \wp_count_posts( 'ap_outbox' )->publish; + $this->assertEquals( $total_before, $total_after, 'No Delete activity should be added for invalid actors' ); + } } From 28804b22431a349527d8756327a1f6d106dc2efc Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 26 Aug 2025 08:39:33 +0200 Subject: [PATCH 15/18] Remove redundant error code check in Tombstone class Eliminated a duplicate check for error codes in the Tombstone class. The logic now relies solely on the error data's status field for validation. --- includes/class-tombstone.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/includes/class-tombstone.php b/includes/class-tombstone.php index 06a5a7ee6..4e69a3a30 100644 --- a/includes/class-tombstone.php +++ b/includes/class-tombstone.php @@ -103,10 +103,6 @@ public static function is_wp_error( $wp_error ) { return false; } - if ( in_array( (int) $wp_error->get_error_code(), self::$codes, true ) ) { - return true; - } - $data = $wp_error->get_error_data(); if ( isset( $data['status'] ) && in_array( (int) $data['status'], self::$codes, true ) ) { return true; From 1d00bebb31e1cbbe8d7eac964a54c1f87f042b98 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 26 Aug 2025 09:07:39 +0200 Subject: [PATCH 16/18] Refactor Tombstone API to use 'exists' naming Renamed Tombstone methods from 'is_gone', 'is_remote_url_gone', 'is_local_url_gone', 'is_wp_error', 'is_array_gone', and 'is_object_gone' to 'exists', 'exists_remote', 'exists_local', 'exists_in_error', 'check_array', and 'check_object' for clarity and consistency. Updated all usages and related tests to match the new method names. Improves code readability and better reflects the purpose of the methods. --- includes/class-activitypub.php | 2 +- includes/class-http.php | 4 +- includes/class-scheduler.php | 2 +- includes/class-tombstone.php | 39 +++++++++------ includes/collection/class-followers.php | 2 +- includes/handler/class-delete.php | 6 +-- tests/includes/class-test-tombstone.php | 66 ++++++++++++++----------- 7 files changed, 69 insertions(+), 52 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 515e8eb04..6cb119b01 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -110,7 +110,7 @@ public static function render_activitypub_template( $template ) { return $template; } - if ( Tombstone::is_local_url_gone( Query::get_instance()->get_request_url() ) ) { + if ( Tombstone::exists_local( Query::get_instance()->get_request_url() ) ) { \status_header( 410 ); return ACTIVITYPUB_PLUGIN_DIR . 'templates/tombstone-json.php'; } diff --git a/includes/class-http.php b/includes/class-http.php index ba23e97fa..d4594c8ed 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -182,9 +182,9 @@ public static function get( $url, $cached = false ) { * @return bool True if the URL is a tombstone. */ public static function is_tombstone( $url ) { - _deprecated_function( __METHOD__, 'unreleased', 'Activitypub\Tombstone::is_remote_url_gone' ); + _deprecated_function( __METHOD__, 'unreleased', 'Activitypub\Tombstone::exists_remote' ); - return Tombstone::is_remote_url_gone( $url ); + return Tombstone::exists_remote( $url ); } /** diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 7243f1b2f..0db20bc36 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -194,7 +194,7 @@ public static function cleanup_remote_actors() { foreach ( $actors as $actor ) { $meta = get_remote_metadata_by_actor( $actor->guid, false ); - if ( Tombstone::is_gone( $meta ) ) { + if ( Tombstone::exists( $meta ) ) { \wp_delete_post( $actor->ID ); } elseif ( empty( $meta ) || ! is_array( $meta ) || \is_wp_error( $meta ) ) { if ( Actors::count_errors( $actor->ID ) >= 5 ) { diff --git a/includes/class-tombstone.php b/includes/class-tombstone.php index 4e69a3a30..82a52fbaa 100644 --- a/includes/class-tombstone.php +++ b/includes/class-tombstone.php @@ -11,6 +11,9 @@ /** * ActivityPub Tombstone Class. + * + * Provides methods to detect deleted/tombstoned ActivityPub resources + * across various data formats (URLs, objects, arrays, WP_Error responses). */ class Tombstone { /** @@ -21,43 +24,47 @@ class Tombstone { private static $codes = array( 404, 410 ); /** - * Check for Tombstone. + * Check if a tombstone exists for the given resource. + * + * Accepts URLs, WP_Error objects, ActivityPub arrays, or objects and + * determines if they indicate a deleted resource (HTTP 404/410 or + * ActivityPub Tombstone type). * * @param string|\WP_Error|array|object $various The various data to check. * - * @return bool True if the various data is a tombstone. + * @return bool True if a tombstone exists for the resource. */ - public static function is_gone( $various ) { + public static function exists( $various ) { if ( \is_wp_error( $various ) ) { - return self::is_wp_error( $various ); + return self::exists_in_error( $various ); } if ( \is_string( $various ) ) { if ( is_same_domain( $various ) ) { - return self::is_local_url_gone( $various ); + return self::exists_local( $various ); } - return self::is_remote_url_gone( $various ); + return self::exists_remote( $various ); } if ( \is_array( $various ) ) { - return self::is_array_gone( $various ); + return self::check_array( $various ); } if ( \is_object( $various ) ) { - return self::is_object_gone( $various ); + return self::check_object( $various ); } return false; } /** - * Check for remote URL for Tombstone. + * Check if remote URL is tombstoned. * * @param string $url The URL to check. * * @return bool True if the URL is a tombstone. */ - public static function is_remote_url_gone( $url ) { + public static function exists_remote( $url ) { /** * Fires before checking if the URL is a tombstone. * @@ -75,17 +82,17 @@ public static function is_remote_url_gone( $url ) { $data = \wp_remote_retrieve_body( $response ); $data = \json_decode( $data, true ); - return self::is_array_gone( $data ); + return self::check_array( $data ); } /** - * Check for local URL for Tombstone. + * Check if local URL is tombstoned. * * @param string $url The URL to check. * * @return bool True if the URL is a tombstone. */ - public static function is_local_url_gone( $url ) { + public static function exists_local( $url ) { $urls = get_option( 'activitypub_tombstone_urls', array() ); return in_array( normalize_url( $url ), $urls, true ); @@ -98,7 +105,7 @@ public static function is_local_url_gone( $url ) { * * @return bool True if the response is a WP_Error, false otherwise. */ - public static function is_wp_error( $wp_error ) { + public static function exists_in_error( $wp_error ) { if ( ! \is_wp_error( $wp_error ) ) { return false; } @@ -118,7 +125,7 @@ public static function is_wp_error( $wp_error ) { * * @return bool True if the array represents a tombstone, false otherwise. */ - public static function is_array_gone( $data ) { + private static function check_array( $data ) { if ( ! \is_array( $data ) ) { return false; } @@ -137,7 +144,7 @@ public static function is_array_gone( $data ) { * * @return bool True if the object represents a tombstone, false otherwise. */ - public static function is_object_gone( $data ) { + private static function check_object( $data ) { if ( ! \is_object( $data ) ) { return false; } diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 944eb81a9..f83ecc062 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -43,7 +43,7 @@ class Followers { public static function add_follower( $user_id, $actor ) { $meta = get_remote_metadata_by_actor( $actor ); - if ( Tombstone::is_gone( $meta ) ) { + if ( Tombstone::exists( $meta ) ) { return $meta; } diff --git a/includes/handler/class-delete.php b/includes/handler/class-delete.php index a9306b49e..b30f2f988 100644 --- a/includes/handler/class-delete.php +++ b/includes/handler/class-delete.php @@ -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 ) && Tombstone::is_gone( $activity['actor'] ) ) { + if ( ! is_wp_error( $follower ) && Tombstone::exists( $activity['actor'] ) ) { Actors::delete( $follower->ID ); self::maybe_delete_interactions( $activity ); } @@ -117,7 +117,7 @@ public static function maybe_delete_follower( $activity ) { */ public static function maybe_delete_interactions( $activity ) { // Verify that Actor is deleted. - if ( Tombstone::is_gone( $activity['actor'] ) ) { + if ( Tombstone::exists( $activity['actor'] ) ) { \wp_schedule_single_event( \time(), 'activitypub_delete_actor_interactions', @@ -153,7 +153,7 @@ public static function maybe_delete_interaction( $activity ) { $comments = Interactions::get_interaction_by_id( $id ); - if ( $comments && Tombstone::is_gone( $id ) ) { + if ( $comments && Tombstone::exists( $id ) ) { foreach ( $comments as $comment ) { wp_delete_comment( $comment->comment_ID, true ); } diff --git a/tests/includes/class-test-tombstone.php b/tests/includes/class-test-tombstone.php index 0e978a714..b24875d72 100644 --- a/tests/includes/class-test-tombstone.php +++ b/tests/includes/class-test-tombstone.php @@ -19,29 +19,29 @@ class Test_Tombstone extends \WP_UnitTestCase { /** * Response code is 404 -> is_tombstone returns true * - * @covers ::is_remote_url_gone + * @covers ::exists_remote * - * @dataProvider data_is_remote_url_gone + * @dataProvider data_exists_remote * * @param array $request The request array. * @param bool $result The expected result. */ - public function test_is_remote_url_gone( $request, $result ) { + public function test_exists_remote( $request, $result ) { $fake_request = function () use ( $request ) { return $request; }; add_filter( 'pre_http_request', $fake_request, 10, 3 ); - $response = Tombstone::is_remote_url_gone( 'https://fake.test/object/123' ); + $response = Tombstone::exists_remote( 'https://fake.test/object/123' ); $this->assertEquals( $result, $response ); remove_filter( 'pre_http_request', $fake_request, 10 ); } /** - * Data provider for test_is_tombstone. + * Data provider for test_exists_remote. * * @return array */ - public function data_is_remote_url_gone() { + public function data_exists_remote() { return array( array( array( 'response' => array( 'code' => 404 ) ), true ), array( array( 'response' => array( 'code' => 410 ) ), true ), @@ -86,68 +86,78 @@ public function data_is_remote_url_gone() { /** * Response code is 404 -> is_tombstone returns true * - * @covers ::is_wp_error + * @covers ::exists_in_error */ - public function test_is_wp_error() { - $response = Tombstone::is_wp_error( new \WP_Error( 404 ) ); - $this->assertTrue( $response ); + public function test_exists_in_error() { + $response = Tombstone::exists_in_error( new \WP_Error( 404 ) ); + $this->assertFalse( $response ); - $response = Tombstone::is_wp_error( new \WP_Error( 410 ) ); - $this->assertTrue( $response ); + $response = Tombstone::exists_in_error( new \WP_Error( 410 ) ); + $this->assertFalse( $response ); - $response = Tombstone::is_wp_error( new \WP_Error( 200 ) ); + $response = Tombstone::exists_in_error( new \WP_Error( 200 ) ); $this->assertFalse( $response ); - $response = Tombstone::is_wp_error( new \WP_Error( 'foo', '', array( 'status' => 404 ) ) ); + $response = Tombstone::exists_in_error( new \WP_Error( 'foo', '', array( 'status' => 404 ) ) ); $this->assertTrue( $response ); - $response = Tombstone::is_wp_error( new \WP_Error( 'bar', '', array( 'status' => 410 ) ) ); + $response = Tombstone::exists_in_error( new \WP_Error( 'bar', '', array( 'status' => 410 ) ) ); $this->assertTrue( $response ); - $response = Tombstone::is_wp_error( new \WP_Error( 'baz', '', array( 'status' => 200 ) ) ); + $response = Tombstone::exists_in_error( new \WP_Error( 'baz', '', array( 'status' => 200 ) ) ); $this->assertFalse( $response ); } /** * Response code is 404 -> is_tombstone returns true * - * @covers ::is_array_gone + * @covers ::check_array */ - public function test_is_array_gone() { - $response = Tombstone::is_array_gone( array( 'type' => 'Tombstone' ) ); + public function test_check_array() { + // Use reflection to access the private method. + $reflection = new \ReflectionClass( Tombstone::class ); + $method = $reflection->getMethod( 'check_array' ); + $method->setAccessible( true ); + + $response = $method->invokeArgs( null, array( array( 'type' => 'Tombstone' ) ) ); $this->assertTrue( $response ); - $response = Tombstone::is_array_gone( array( 'type' => 'Note' ) ); + $response = $method->invokeArgs( null, array( array( 'type' => 'Note' ) ) ); $this->assertFalse( $response ); } /** * Response code is 404 -> is_tombstone returns true * - * @covers ::is_object_gone + * @covers ::check_object */ - public function test_is_object_gone() { - $response = Tombstone::is_object_gone( (object) array( 'type' => 'Tombstone' ) ); + public function test_check_object() { + // Use reflection to access the private method. + $reflection = new \ReflectionClass( Tombstone::class ); + $method = $reflection->getMethod( 'check_object' ); + $method->setAccessible( true ); + + $response = $method->invokeArgs( null, array( (object) array( 'type' => 'Tombstone' ) ) ); $this->assertTrue( $response ); - $response = Tombstone::is_object_gone( (object) array( 'type' => 'Note' ) ); + $response = $method->invokeArgs( null, array( (object) array( 'type' => 'Note' ) ) ); $this->assertFalse( $response ); } /** * Response code is 404 -> is_tombstone returns true * - * @covers ::is_local_url_gone + * @covers ::exists_local */ - public function test_is_local_url_gone() { + public function test_exists_local() { $url = 'https://fake.test/object/123'; - $response = Tombstone::is_local_url_gone( $url ); + $response = Tombstone::exists_local( $url ); $this->assertFalse( $response ); Tombstone::bury( $url ); - $response = Tombstone::is_local_url_gone( $url ); + $response = Tombstone::exists_local( $url ); $this->assertTrue( $response ); \delete_option( 'activitypub_tombstone_urls' ); From 576cf22b0504f1fd36573747dc8973c2ed8e585d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 26 Aug 2025 09:13:46 +0200 Subject: [PATCH 17/18] Update deprecated function reference in is_tombstone Replaces the deprecated function notice and call from Tombstone::is_wp_error to Tombstone::exists_in_error to reflect the updated method name. --- includes/functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 575d90961..7733dd09a 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -204,9 +204,9 @@ function is_comment() { * @return boolean True if HTTP-Code is 410 or 404. */ function is_tombstone( $wp_error ) { - _deprecated_function( __FUNCTION__, 'unreleased', 'Activitypub\Tombstone::is_wp_error' ); + _deprecated_function( __FUNCTION__, 'unreleased', 'Activitypub\Tombstone::exists_in_error' ); - return Tombstone::is_wp_error( $wp_error ); + return Tombstone::exists_in_error( $wp_error ); } /** From 04273e44bccddb456e1841ae2a7c4b06ecb38cd0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 26 Aug 2025 09:21:03 +0200 Subject: [PATCH 18/18] Improve and expand docblocks in Tombstone class Enhanced and clarified docblocks for all methods and the class itself in includes/class-tombstone.php. The new comments provide more detailed descriptions, parameter explanations, and context for tombstone detection logic, improving code readability and maintainability. --- includes/class-tombstone.php | 87 +++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/includes/class-tombstone.php b/includes/class-tombstone.php index 82a52fbaa..42d8e37af 100644 --- a/includes/class-tombstone.php +++ b/includes/class-tombstone.php @@ -12,27 +12,38 @@ /** * ActivityPub Tombstone Class. * - * Provides methods to detect deleted/tombstoned ActivityPub resources - * across various data formats (URLs, objects, arrays, WP_Error responses). + * Handles detection and management of tombstoned (deleted) ActivityPub resources. + * A tombstone in ActivityPub represents a deleted object that was previously available. + * This class provides methods to detect tombstones across various data formats including + * URLs, ActivityPub objects, arrays, and WordPress error responses. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone */ class Tombstone { /** - * HTTP codes that indicate a tombstone. + * HTTP status codes that indicate a tombstoned resource. + * + * - 404: Not Found - Resource no longer exists + * - 410: Gone - Resource was intentionally removed * - * @var array + * @var int[] Array of HTTP status codes indicating tombstones. */ private static $codes = array( 404, 410 ); /** * Check if a tombstone exists for the given resource. * - * Accepts URLs, WP_Error objects, ActivityPub arrays, or objects and - * determines if they indicate a deleted resource (HTTP 404/410 or - * ActivityPub Tombstone type). + * This is the main entry point for tombstone detection. It accepts various + * data types and routes them to the appropriate checking method: + * - URLs (string): Checks remote or local tombstone status + * - WP_Error objects: Checks for tombstone-indicating HTTP status codes + * - Arrays: Checks for ActivityPub Tombstone type + * - Objects: Checks for ActivityPub Tombstone type or Base_Object instances * - * @param string|\WP_Error|array|object $various The various data to check. + * @param string|\WP_Error|array|object $various The resource data to check for tombstone status. + * Can be a URL, error object, ActivityPub array, or object. * - * @return bool True if a tombstone exists for the resource. + * @return bool True if the resource is tombstoned, false otherwise. */ public static function exists( $various ) { if ( \is_wp_error( $various ) ) { @@ -58,11 +69,16 @@ public static function exists( $various ) { } /** - * Check if remote URL is tombstoned. + * Check if a remote URL is tombstoned. + * + * Makes an HTTP request to the remote URL with ActivityPub headers + * and checks for tombstone indicators: + * - HTTP 404/410 status codes + * - ActivityPub Tombstone object type in response body * - * @param string $url The URL to check. + * @param string $url The remote URL to check for tombstone status. * - * @return bool True if the URL is a tombstone. + * @return bool True if the remote URL is tombstoned, false otherwise. */ public static function exists_remote( $url ) { /** @@ -86,11 +102,14 @@ public static function exists_remote( $url ) { } /** - * Check if local URL is tombstoned. + * Check if a local URL is tombstoned. * - * @param string $url The URL to check. + * Checks against the local tombstone URL registry stored in WordPress options. + * Local URLs are normalized before comparison to ensure consistent matching. * - * @return bool True if the URL is a tombstone. + * @param string $url The local URL to check for tombstone status. + * + * @return bool True if the local URL is in the tombstone registry, false otherwise. */ public static function exists_local( $url ) { $urls = get_option( 'activitypub_tombstone_urls', array() ); @@ -99,11 +118,14 @@ public static function exists_local( $url ) { } /** - * Check if the response is a WP_Error. + * Check if a WP_Error object indicates a tombstoned resource. + * + * Examines the error data for HTTP status codes that indicate tombstones. + * This is typically used when HTTP requests return error responses. * - * @param \WP_Error $wp_error The response to check. + * @param \WP_Error $wp_error The WordPress error object to examine. * - * @return bool True if the response is a WP_Error, false otherwise. + * @return bool True if the error indicates a tombstoned resource, false otherwise. */ public static function exists_in_error( $wp_error ) { if ( ! \is_wp_error( $wp_error ) ) { @@ -119,11 +141,14 @@ public static function exists_in_error( $wp_error ) { } /** - * Check if the given array represents a tombstone. + * Check if an array represents an ActivityPub Tombstone object. * - * @param array $data The array to check. + * Examines the array for the ActivityPub 'type' property set to 'Tombstone'. + * This follows the ActivityStreams specification for tombstone objects. * - * @return bool True if the array represents a tombstone, false otherwise. + * @param array|mixed $data The array data to check. Non-arrays return false. + * + * @return bool True if the array represents a Tombstone object, false otherwise. */ private static function check_array( $data ) { if ( ! \is_array( $data ) ) { @@ -138,11 +163,15 @@ private static function check_array( $data ) { } /** - * Check if the given object represents a tombstone. + * Check if an object represents an ActivityPub Tombstone. + * + * Checks for tombstone indicators in objects: + * - Standard objects: 'type' property set to 'Tombstone' + * - Base_Object instances: Uses get_type() method to check for 'Tombstone' * - * @param object $data The object to check. + * @param object|mixed $data The object data to check. Non-objects return false. * - * @return bool True if the object represents a tombstone, false otherwise. + * @return bool True if the object represents a Tombstone, false otherwise. */ private static function check_object( $data ) { if ( ! \is_object( $data ) ) { @@ -161,9 +190,15 @@ private static function check_object( $data ) { } /** - * Bury a URL. + * Add a URL to the local tombstone registry. + * + * "Buries" a URL by adding it to the local tombstone URL registry. + * The URL is normalized before storage and duplicates are automatically removed. + * This marks the URL as tombstoned for future local checks. + * + * @param string $url The URL to add to the tombstone registry. * - * @param string $url The URL to bury. + * @return void */ public static function bury( $url ) { $urls = \get_option( 'activitypub_tombstone_urls', array() );