From 87ed60d675f519941d8b0a5d0cfea92227c68c68 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 29 Aug 2026 22:42:12 +0930 Subject: [PATCH] Accept compacted Note attachments (#6588) Normalize JSON-LD compacted single attachments (a bare object instead of a one-item array) in getAttachments(), and route verifyAttachments() through it so validation and import share one normalization path. Includes PR #6589's tests plus additional edge-case coverage: list-form preservation, bare-input normalization, and guards for missing/empty/scalar attachments. --- app/Util/ActivityPub/Helpers.php | 28 +++- tests/Unit/ActivityPub/NoteAttachmentTest.php | 126 ++++++++++++++++++ 2 files changed, 148 insertions(+), 6 deletions(-) diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index c10452c17..708409157 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -80,17 +80,18 @@ class Helpers $data = ['object' => $data]; } - $activity = $data['object']; $mimeTypes = explode(',', config_cache('pixelfed.media_types')); $mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video'] : ['Document', 'Image']; - if (! isset($activity['attachment']) || empty($activity['attachment'])) { + $attachments = self::getAttachments($data); + + if (empty($attachments)) { return false; } - return Validator::make($activity['attachment'], [ + return Validator::make($attachments, [ '*.type' => ['required', 'string', Rule::in($mediaTypes)], '*.url' => 'required|url', '*.mediaType' => ['required', 'string', Rule::in($mimeTypes)], @@ -1155,9 +1156,24 @@ class Helpers */ public static function getAttachments(array $data): array { - return isset($data['object']) ? - $data['object']['attachment'] : - $data['attachment']; + $object = isset($data['object']) ? + $data['object'] : + $data; + + if (! is_array($object) || + ! isset($object['attachment']) || + empty($object['attachment']) || + ! is_array($object['attachment']) + ) { + return []; + } + + // JSON-LD compaction can collapse a single-item attachment array into a + // bare object. Normalize both shapes to a list so callers can iterate + // uniformly (pixelfed#6588). + return array_is_list($object['attachment']) ? + $object['attachment'] : + [$object['attachment']]; } /** diff --git a/tests/Unit/ActivityPub/NoteAttachmentTest.php b/tests/Unit/ActivityPub/NoteAttachmentTest.php index f9bb95570..9614ff0a7 100644 --- a/tests/Unit/ActivityPub/NoteAttachmentTest.php +++ b/tests/Unit/ActivityPub/NoteAttachmentTest.php @@ -60,4 +60,130 @@ class NoteAttachmentTest extends TestCase $valid = Helpers::verifyAttachments($this->invalidMime); $this->assertFalse($valid); } + + // --- Compacted (single JSON-LD object instead of a one-item array) attachments (#6588) --- + + #[Test] + public function compacted_create_note_attachment() + { + $activity = $this->mastodon; + $activity['object']['attachment'] = $activity['object']['attachment'][0]; + + $valid = Helpers::verifyAttachments($activity); + $this->assertTrue($valid); + } + + #[Test] + public function compacted_bare_note_attachment() + { + $activity = $this->pixelfed; + $activity['attachment'] = $activity['attachment'][0]; + + $valid = Helpers::verifyAttachments($activity); + $this->assertTrue($valid); + } + + #[Test] + public function compacted_invalid_attachment_type() + { + $activity = $this->invalidType; + $activity['object']['attachment'] = $activity['object']['attachment'][0]; + + $valid = Helpers::verifyAttachments($activity); + $this->assertFalse($valid); + } + + #[Test] + public function compacted_invalid_mime_type() + { + $activity = $this->invalidMime; + $activity['object']['attachment'] = $activity['object']['attachment'][0]; + + $valid = Helpers::verifyAttachments($activity); + $this->assertFalse($valid); + } + + #[Test] + public function get_attachments_returns_list_for_compacted_attachment() + { + $activity = $this->mastodon; + $activity['object']['attachment'] = $activity['object']['attachment'][0]; + + $attachments = Helpers::getAttachments($activity); + + $this->assertCount(1, $attachments); + $this->assertSame('Document', $attachments[0]['type']); + } + + // --- Additional edge cases --- + + #[Test] + public function get_attachments_preserves_list_form() + { + // A normal array-form attachment list must pass through unchanged. + $attachments = Helpers::getAttachments($this->pleroma); + + $this->assertCount(2, $attachments); + $this->assertSame('Document', $attachments[0]['type']); + $this->assertSame('Document', $attachments[1]['type']); + } + + #[Test] + public function get_attachments_returns_empty_for_bare_input() + { + // Bare Note (no "object" wrapper) with a compacted attachment normalizes to one item. + $activity = $this->pixelfed; + $activity['attachment'] = $activity['attachment'][0]; + + $attachments = Helpers::getAttachments($activity); + + $this->assertCount(1, $attachments); + $this->assertSame('Image', $attachments[0]['type']); + } + + #[Test] + public function get_attachments_returns_empty_when_missing() + { + $activity = $this->mastodon; + unset($activity['object']['attachment']); + + $this->assertSame([], Helpers::getAttachments($activity)); + } + + #[Test] + public function get_attachments_returns_empty_when_empty_array() + { + $activity = $this->mastodon; + $activity['object']['attachment'] = []; + + $this->assertSame([], Helpers::getAttachments($activity)); + } + + #[Test] + public function get_attachments_returns_empty_for_scalar_attachment() + { + // A non-array attachment (e.g. a bare URL string) must not blow up. + $activity = $this->mastodon; + $activity['object']['attachment'] = 'https://example.org/not-an-object.jpg'; + + $this->assertSame([], Helpers::getAttachments($activity)); + } + + #[Test] + public function verify_attachments_false_for_missing_attachment() + { + $activity = $this->mastodon; + unset($activity['object']['attachment']); + + $this->assertFalse(Helpers::verifyAttachments($activity)); + } + + #[Test] + public function verify_attachments_false_for_scalar_attachment() + { + $activity = $this->mastodon; + $activity['object']['attachment'] = 'https://example.org/not-an-object.jpg'; + + $this->assertFalse(Helpers::verifyAttachments($activity)); + } }