diff --git a/CHANGELOG.md b/CHANGELOG.md index 27dd9115d..322fb55c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Bumped minimum required WordPress version to 6.4. * Use a later hook for Posts to get published to the Outbox, to get sure all `post_meta`s and `taxonomy`s are set stored properly. +* Use webfinger as author email for comments from the Fediverse. ## [5.3.2] - 2025-02-27 diff --git a/includes/class-migration.php b/includes/class-migration.php index d3453a829..4e859596f 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -185,6 +185,9 @@ public static function maybe_migrate() { if ( \version_compare( $version_from_db, '5.3.0', '<' ) ) { add_action( 'init', 'flush_rewrite_rules', 20 ); } + if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) { + \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_comment_author_emails' ) ); + } /* * Add new update routines above this comment. ^ @@ -632,6 +635,57 @@ public static function create_comment_outbox_items( $batch_size = 50, $offset = return null; } + /** + * Update comment author emails with webfinger addresses for ActivityPub comments. + * + * @param int $batch_size Optional. Number of comments to process per batch. Default 50. + * @param int $offset Optional. Number of comments to skip. Default 0. + * @return array|null Array with batch size and offset if there are more comments to process, null otherwise. + */ + public static function update_comment_author_emails( $batch_size = 50, $offset = 0 ) { + $comments = \get_comments( + array( + 'number' => $batch_size, + 'offset' => $offset, + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + array( + 'key' => 'protocol', + 'value' => 'activitypub', + ), + ), + ) + ); + + foreach ( $comments as $comment ) { + $comment_author_url = $comment->comment_author_url; + if ( empty( $comment_author_url ) ) { + continue; + } + + $webfinger = Webfinger::uri_to_acct( $comment_author_url ); + if ( \is_wp_error( $webfinger ) ) { + continue; + } + + \wp_update_comment( + array( + 'comment_ID' => $comment->comment_ID, + 'comment_author_email' => \str_replace( 'acct:', '', $webfinger ), + ) + ); + } + + if ( count( $comments ) === $batch_size ) { + return array( + 'batch_size' => $batch_size, + 'offset' => $offset + $batch_size, + ); + } + + return null; + } + /** * Set the defaults needed for the plugin to work. * diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index f73e85eec..df4b710e5 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -84,6 +84,8 @@ public static function resolve( $uri ) { /** * Transform a URI to an acct @. * + * @see https://swicg.github.io/activitypub-webfinger/#reverse-discovery + * * @param string $uri The URI (acct:, mailto:, http:, https:). * * @return string|WP_Error Error or acct URI. diff --git a/includes/collection/class-interactions.php b/includes/collection/class-interactions.php index 9c1001b4f..4f08702b9 100644 --- a/includes/collection/class-interactions.php +++ b/includes/collection/class-interactions.php @@ -7,6 +7,7 @@ namespace Activitypub\Collection; +use Activitypub\Webfinger; use WP_Comment_Query; use Activitypub\Comment; @@ -256,12 +257,19 @@ public static function activity_to_comment( $activity ) { $comment_content = \addslashes( $activity['object']['content'] ); } + $webfinger = Webfinger::uri_to_acct( $url ); + if ( is_wp_error( $webfinger ) ) { + $webfinger = ''; + } else { + $webfinger = str_replace( 'acct:', '', $webfinger ); + } + $commentdata = array( 'comment_author' => \esc_attr( $comment_author ), 'comment_author_url' => \esc_url_raw( $url ), 'comment_content' => $comment_content, 'comment_type' => 'comment', - 'comment_author_email' => '', + 'comment_author_email' => $webfinger, 'comment_meta' => array( 'source_id' => \esc_url_raw( object_to_uri( $activity['object'] ) ), 'protocol' => 'activitypub', diff --git a/readme.txt b/readme.txt index e817ded58..9ddd8520c 100644 --- a/readme.txt +++ b/readme.txt @@ -133,6 +133,7 @@ For reasons of data protection, it is not possible to see the followers of other * Changed: Bumped minimum required WordPress version to 6.4. * Changed: Use a later hook for Posts to get published to the Outbox, to get sure all `post_meta`s and `taxonomy`s are set stored properly. +* Changed: Use webfinger as author email for comments from the Fediverse. = 5.3.2 = diff --git a/tests/includes/class-test-migration.php b/tests/includes/class-test-migration.php index ee672b7f0..a5c979c7c 100644 --- a/tests/includes/class-test-migration.php +++ b/tests/includes/class-test-migration.php @@ -597,4 +597,99 @@ public function test_create_comment_outbox_items_batching() { $result = Migration::create_comment_outbox_items( 1, 1000 ); $this->assertNull( $result ); } + + /** + * Test update_comment_author_emails updates emails with webfinger addresses. + * + * @covers ::update_comment_author_emails + */ + public function test_update_comment_author_emails() { + $author_url = 'https://example.com/users/test'; + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$fixtures['posts'][0], + 'comment_author' => 'Test User', + 'comment_author_url' => $author_url, + 'comment_author_email' => '', + 'comment_type' => 'comment', + 'comment_meta' => array( 'protocol' => 'activitypub' ), + ) + ); + + // Mock the HTTP request. + \add_filter( 'pre_http_request', array( $this, 'mock_webfinger' ) ); + + $result = Migration::update_comment_author_emails( 50, 0 ); + + $this->assertNull( $result ); + + $updated_comment = \get_comment( $comment_id ); + $this->assertEquals( 'test@example.com', $updated_comment->comment_author_email ); + + // Clean up. + \remove_filter( 'pre_http_request', array( $this, 'mock_webfinger' ) ); + \wp_delete_comment( $comment_id, true ); + } + + /** + * Test update_comment_author_emails handles batching correctly. + * + * @covers ::update_comment_author_emails + */ + public function test_update_comment_author_emails_batching() { + // Create multiple comments. + $comment_ids = array(); + for ( $i = 0; $i < 3; $i++ ) { + $comment_ids[] = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$fixtures['posts'][0], + 'comment_author' => "Test User $i", + 'comment_author_url' => "https://example.com/users/test$i", + 'comment_author_email' => '', + 'comment_content' => "Test comment $i", + 'comment_type' => 'comment', + 'comment_meta' => array( 'protocol' => 'activitypub' ), + ) + ); + } + + // Mock the HTTP request. + \add_filter( 'pre_http_request', array( $this, 'mock_webfinger' ) ); + + // Process first batch of 2 comments. + $result = Migration::update_comment_author_emails( 2, 0 ); + $this->assertEqualSets( + array( + 'batch_size' => 2, + 'offset' => 2, + ), + $result + ); + + // Process second batch with remaining comment. + $result = Migration::update_comment_author_emails( 2, 2 ); + $this->assertNull( $result ); + + // Verify all comments were updated. + foreach ( $comment_ids as $comment_id ) { + $comment = \get_comment( $comment_id ); + $this->assertEquals( 'test@example.com', $comment->comment_author_email ); + + wp_delete_comment( $comment_id, true ); + } + + \remove_filter( 'pre_http_request', array( $this, 'mock_webfinger' ) ); + } + + /** + * Mock webfinger response. + * + * @return array + */ + public function mock_webfinger() { + return array( + 'body' => wp_json_encode( array( 'subject' => 'acct:test@example.com' ) ), + 'response' => array( 'code' => 200 ), + ); + } } diff --git a/tests/includes/collection/class-test-interactions.php b/tests/includes/collection/class-test-interactions.php index dee6e8dbf..d06f76991 100644 --- a/tests/includes/collection/class-test-interactions.php +++ b/tests/includes/collection/class-test-interactions.php @@ -471,4 +471,35 @@ function () { remove_all_filters( 'pre_get_remote_metadata_by_actor' ); } + + /** + * Test activity_to_comment sets webfinger as comment author email. + * + * @covers ::activity_to_comment + */ + public function test_activity_to_comment_sets_webfinger_email() { + $actor_url = 'https://example.com/users/tester'; + $activity = array( + 'type' => 'Create', + 'actor' => $actor_url, + 'object' => array( + 'content' => 'Test comment content', + 'id' => 'https://example.com/activities/1', + ), + ); + + $filter = function () { + return array( + 'body' => wp_json_encode( array( 'subject' => 'acct:tester@example.com' ) ), + 'response' => array( 'code' => 200 ), + ); + }; + \add_filter( 'pre_http_request', $filter ); + + $comment_data = Interactions::activity_to_comment( $activity ); + + $this->assertEquals( 'tester@example.com', $comment_data['comment_author_email'] ); + + \remove_filter( 'pre_http_request', $filter ); + } }