diff --git a/.github/changelog/2874-from-description b/.github/changelog/2874-from-description new file mode 100644 index 0000000000..b608462eca --- /dev/null +++ b/.github/changelog/2874-from-description @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Ensure Extra Fields links always include `rel="me"` for Mastodon verification. diff --git a/includes/collection/class-extra-fields.php b/includes/collection/class-extra-fields.php index 2fad0fad38..ab6649d2b9 100644 --- a/includes/collection/class-extra-fields.php +++ b/includes/collection/class-extra-fields.php @@ -107,8 +107,12 @@ public static function fields_to_attachments( $fields ) { \add_filter( 'activitypub_link_rel', array( self::class, 'add_rel_me' ) ); foreach ( $fields as $post ) { - $title = \html_entity_decode( \get_the_title( $post ), \ENT_QUOTES, 'UTF-8' ); - $content = self::get_formatted_content( $post ); + $title = \html_entity_decode( \get_the_title( $post ), \ENT_QUOTES, 'UTF-8' ); + $content = self::get_formatted_content( $post ); + + // Ensure all links have rel="me" for verification (both in HTML and Link attachment). + $content = self::add_rel_me_to_links( $content ); + $attachments[] = array( 'type' => 'PropertyValue', 'name' => $title, @@ -292,6 +296,36 @@ public static function add_rel_me( $rel ) { return $rel . ' me'; } + /** + * Add rel="me" to all links in HTML content. + * + * This ensures verification works for links that were already wrapped + * in tags (e.g., from the block editor) and didn't go through + * the activitypub_link_rel filter. + * + * @param string $content The HTML content. + * @return string The content with rel="me" added to all links. + */ + public static function add_rel_me_to_links( $content ) { + if ( ! \class_exists( '\WP_HTML_Tag_Processor' ) ) { + return $content; + } + + $tags = new \WP_HTML_Tag_Processor( $content ); + + while ( $tags->next_tag( 'A' ) ) { + $rel = $tags->get_attribute( 'rel' ); + $rel_parts = $rel && \is_string( $rel ) ? \explode( ' ', $rel ) : array(); + + if ( ! \in_array( 'me', $rel_parts, true ) ) { + $rel_parts[] = 'me'; + $tags->set_attribute( 'rel', \implode( ' ', $rel_parts ) ); + } + } + + return $tags->get_updated_html(); + } + /** * Checks if the user is the blog user. * diff --git a/tests/phpunit/tests/includes/collection/class-test-extra-fields.php b/tests/phpunit/tests/includes/collection/class-test-extra-fields.php index 0b78f646c1..2dae76bfb2 100644 --- a/tests/phpunit/tests/includes/collection/class-test-extra-fields.php +++ b/tests/phpunit/tests/includes/collection/class-test-extra-fields.php @@ -39,6 +39,92 @@ public function test_get_attachment() { $this->assertEquals( 1, $value_count['me'] ); } + /** + * Test that pre-linked URLs get rel="me" added to both PropertyValue and Link. + * + * When users create extra fields in the block editor, URLs are often + * already wrapped in tags without rel="me". This test ensures + * verification works by always adding "me" to the rel attribute in both + * the PropertyValue HTML and the Link attachment. + * + * @covers ::fields_to_attachments + * @covers ::add_rel_me_to_links + */ + public function test_prelinked_url_gets_rel_me() { + $post = self::factory()->post->create_and_get( + array( + 'post_type' => Extra_Fields::BLOG_POST_TYPE, + 'post_content' => 'Mastodon', + 'post_title' => 'Mastodon', + ) + ); + + $attachments = Extra_Fields::fields_to_attachments( array( $post ) ); + + // The PropertyValue HTML should have rel="me" for verification. + $this->assertEquals( 'PropertyValue', $attachments[0]['type'] ); + $this->assertStringContainsString( 'rel="me"', $attachments[0]['value'] ); + + // The Link attachment should also have rel="me". + $this->assertEquals( 'Link', $attachments[1]['type'] ); + $this->assertEquals( 'https://mastodon.social/@user', $attachments[1]['href'] ); + $this->assertContains( 'me', $attachments[1]['rel'] ); + } + + /** + * Test that pre-linked URLs with existing rel attributes get "me" added. + * + * @covers ::fields_to_attachments + * @covers ::add_rel_me_to_links + */ + public function test_prelinked_url_with_rel_gets_me_added() { + $post = self::factory()->post->create_and_get( + array( + 'post_type' => Extra_Fields::BLOG_POST_TYPE, + 'post_content' => 'Example', + 'post_title' => 'Example', + ) + ); + + $attachments = Extra_Fields::fields_to_attachments( array( $post ) ); + + // The PropertyValue HTML should have rel with "me" added. + $this->assertEquals( 'PropertyValue', $attachments[0]['type'] ); + $this->assertMatchesRegularExpression( '/rel="[^"]*me[^"]*"/', $attachments[0]['value'] ); + $this->assertStringContainsString( 'nofollow', $attachments[0]['value'] ); + + // The Link attachment should have original rel values plus "me". + $this->assertEquals( 'Link', $attachments[1]['type'] ); + $this->assertContains( 'nofollow', $attachments[1]['rel'] ); + $this->assertContains( 'noopener', $attachments[1]['rel'] ); + $this->assertContains( 'me', $attachments[1]['rel'] ); + } + + /** + * Test that pre-linked URLs with existing rel="me" don't get duplicate. + * + * @covers ::fields_to_attachments + * @covers ::add_rel_me_to_links + */ + public function test_prelinked_url_with_rel_me_no_duplicate() { + $post = self::factory()->post->create_and_get( + array( + 'post_type' => Extra_Fields::BLOG_POST_TYPE, + 'post_content' => 'Example', + 'post_title' => 'Example', + ) + ); + + $attachments = Extra_Fields::fields_to_attachments( array( $post ) ); + + // PropertyValue should have "me" exactly once in the HTML. + $this->assertEquals( 1, \substr_count( $attachments[0]['value'], ' me' ) + \substr_count( $attachments[0]['value'], '"me' ) ); + + // Link attachment should have "me" exactly once. + $value_count = array_count_values( $attachments[1]['rel'] ); + $this->assertEquals( 1, $value_count['me'] ); + } + /** * Test that HTML entities are decoded in field names and values. *