Skip to content

Display images attached to Fediverse comments - #3850

Open
pfefferle wants to merge 16 commits into
trunkfrom
fix/comment-images
Open

pfefferle wants to merge 16 commits into
trunkfrom
fix/comment-images

Conversation

@pfefferle

@pfefferle pfefferle commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Fixes #1852

Proposed changes:

  • Add an option "Show images attached to replies" in the ActivityPub settings. It is off by default, because WordPress itself does not show images in comments and this changes how comments look.
  • With the option on, images attached to incoming Fediverse replies are shown below the comment text, including image-only replies and updated attachments. Up to three images per comment, the limit is filterable with activitypub_comment_image_limit. Audio and video are not included.
  • With the option off, nothing changes: no image blocks are stored and image-only replies are rejected like before.
  • I reuse the existing attachment parser, the activitypub/image block and the lazy Media cache. Comment images share the cache directory of their parent post, which is cleaned up when the post is deleted.
  • Image descriptions survive the comment sanitization. Only our own leaf emoji and image blocks are rendered, and image blocks only for received Fediverse comments.
  • Attachment URLs must be absolute HTTP(S) URLs on a public host.

One thing to keep in mind: on sites that auto-approve comments, remote images become visible without moderation once the option is on.

Testing instructions:

  • Enable "Show images attached to replies" under Settings → ActivityPub.
  • Reply to a federated post from Mastodon with an image and an image description. Check that the image and description show up, and that the image URL points to /uploads/activitypub/posts/{post-id}/ after caching.
  • Repeat with an image-only reply, and edit a reply to replace or remove its image.
  • Disable the option and send another reply with an image. The comment should show only the text.
  • Delete the parent post permanently and check that its cached image directory is removed.

Copilot AI balanced review requested due to automatic review settings October 1, 2026 16:37
@pfefferle pfefferle self-assigned this Oct 1, 2026
@pfefferle
pfefferle requested a review from a team October 1, 2026 16:37
@github-actions github-actions Bot added [Feature] Collections [Focus] Editor Changes to the ActivityPub experience in the block editor [Tests] Includes Tests labels Oct 1, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Attachment classification and unconditional URL entity decoding can produce broken or incorrect image resources.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
What changed in this PR

Adds image attachments to incoming Fediverse comments using existing block rendering and media caching.

Changes:

  • Parses, sanitizes, and renders comment image attachments.
  • Shares parent-post media caching and cleanup.
  • Adds coverage for rendering, updates, sanitization, and deletion.
File Description
includes/​functions-media.php Preserves image alt text in block attributes.
includes/​collection/​class-remote-posts.php Exposes and extends attachment parsing.
includes/​collection/​class-interactions.php Adds images to created and updated comments.
includes/​class-comment.php Supplies parent-post context while rendering comments.
includes/​class-blocks.php Reconstructs sanitized images and uses block context.
includes/​cache/​class-media.php Cleans caches for ordinary parent posts.
tests/​phpunit/​tests/​includes/​collection/​class-test-interactions.php Tests comment image lifecycle and sanitization.
tests/​phpunit/​tests/​includes/​class-test-functions-media.php Tests safe block attributes and media filtering.
tests/​phpunit/​tests/​includes/​class-test-comment.php Tests comment block context and filters.
tests/​phpunit/​tests/​includes/​cache/​class-test-media.php Tests regular-post cache cleanup.
.github/​changelog/​fix-comment-images Adds the user-facing changelog entry.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread includes/class-blocks.php Outdated
Comment thread includes/collection/class-remote-posts.php Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Unresolved image-handling issues and changes to untrusted-content rendering require human security review.

Review effort: Balanced
Findings: None

Resolved since last review (2)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Prevent remote image tracking URLs in ordinary WordPress comments

includes/​class-comment.php:74

comment_text also handles ordinary WordPress comments, which do not pass through the remote-content sanitizer. A commenter can submit <!-- wp:activitypub/image {"url":"https://example.com/tracker.jpg","alt":""} /-->; KSES preserves the block attributes, and the new image callback reconstructs an image despite comment HTML restrictions. If caching is disabled or fails, readers load the remote tracking URL. Require a supplied comment with was_received() before rendering image blocks, while preserving emoji rendering. Update the rendering tests to use received-comment metadata and add an ordinary-comment submission regression test.

Medium severity Preserve linked image attachments during comment creation and updates

includes/​collection/​class-interactions.php:528

Both comment creation here and updates at line 164 can omit an image attachment when the reply also contains its URL in text or a link. process_remote_images() skips URLs found anywhere in the content (includes/functions-media.php:171–174), even when sanitization has removed the linked image. Generate attachment blocks with process_remote_images( '', ... ) and append them to the sanitized content at both call sites. Add creation and update tests where the reply links to its attachment.

@pfefferle
pfefferle requested a balanced review from Copilot October 2, 2026 09:50
@pfefferle

Copy link
Copy Markdown
Member Author

Addressed both "Previously missed" findings in 3457443. Image-block rendering now requires a supplied comment recognized by Comment::was_received(); emoji rendering is unchanged. Comment creation and updates append attachment blocks independently of sanitized text, so linked attachment URLs no longer suppress images. Regression coverage includes ordinary comment submission with unavailable caching, missing comment context, emoji compatibility, and plain URL / linked / linked-image attachments on creation and update. Full PHPUnit suite passes (3,464 tests, 25 skipped); PHPStan, PHPCS, and code review pass.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Changes to sanitization and dynamic rendering of untrusted content warrant final human security review.

Review effort: Balanced
Findings: None

Previously missed (1)

In code that hasn't changed since last review

Low severity Prefix wp_delete_post() with a leading backslash

tests/​phpunit/​tests/​includes/​cache/​class-test-media.php:100

Prefix this call with \wp_delete_post() to follow the namespaced WordPress-function convention in AGENTS.md:30. The current call works through PHP's global fallback, so this is a consistency fix rather than a behavior change.

@pfefferle

Copy link
Copy Markdown
Member Author

Fixed the previously missed namespace-convention finding in 9cd8dad: the media cache cleanup test now calls \wp_delete_post(). This is a test-only consistency change with no behavior change. The focused cleanup regression test and PHPCS both pass.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Security-sensitive changes to remote-content sanitization and block rendering need final human validation.

Review effort: Balanced
Findings: None

Previously missed (1)

In code that hasn't changed since last review

Medium severity Add coverage for single-attachment normalization

tests/​phpunit/​tests/​includes/​collection/​class-test-remote-posts.php:41

Every case here supplies attachment as a list, so the new single-attachment normalization in Remote_Posts::extract_attachments() is not covered. Existing single-object tests exercise the separate image fallback instead. Run these cases with a single associative array and a stdClass as well, and assert the URL, description, and type survive normalization. This would catch regressions that silently drop valid comment images.

@pfefferle

Copy link
Copy Markdown
Member Author

Addressed the summary-only finding Add coverage for single-attachment normalization in e630293. The existing classifier test now runs each of seven media/type cases as a list, a single associative attachment, and a stdClass attachment (21 combinations). It asserts the complete normalized result, including URL, description, and type. No production changes or new helpers. Focused attachment/image-fallback tests pass: 7 tests / 27 assertions; PHPCS passes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Validation and description preservation need fixes, and the sanitization changes warrant final human review.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Preserve less-than signs in serialized image alt text

includes/​functions-media.php:63

get_attribute() returns decoded text, but WordPress sanitizes block attribute strings as HTML when saving comments. A description such as width < height can therefore lose everything from the less-than sign onward before the image is rebuilt. HTML-encode alt before serialization, as with url; the renderer already escapes it with esc_attr(), which preserves existing entities. Update the block-generation expectations and add a comment save/render regression containing a literal <.

Comment thread includes/collection/class-interactions.php
@pfefferle

Copy link
Copy Markdown
Member Author

Whole-PR review and feedback assessment for d0892ee:

The earlier tests bypassed inbox validation and did not follow descriptions through comment persistence. That was too narrow. This review traced ingestion, attachment normalization, sanitization, block generation, saving/updating, rendering, and shared-cache cleanup, with an independent review of the full diff.

  • Missing-content validation: valid, fixed by reusing attachment extraction while retaining required IDs and previous validation vetoes. Both inbox routes now have regressions. I assess the impact as P2 functionality, not a demonstrated security vulnerability.
  • Alt-text preservation: valid, fixed by encoding decoded text before block serialization. The wider review also found literal entity spellings were lost earlier during attachment-to-HTML conversion; that step now encodes JSON-derived descriptions before the existing esc_attr() call. The regression starts with attachment.name and follows conversion, comment creation/update, and rendering. Existing HTML stripping remains intact.
  • Rendering boundaries: received-comment metadata is still required for image rendering; ordinary comments cannot reconstruct tracking images from block attributes. Only allowed leaf image/emoji blocks render. The nested-block regression now supplies a received comment, exercising the image-permitted branch instead of trivially rejecting every image.
  • KISS: no new helper classes, cache lifecycle, or generic media support. Removed duplicated image-fallback normalization; the common normalization pass already handles it.

Verification: full PHPUnit suite passed (3,474 tests, 8,519 assertions, 25 skipped); final PR-focused suite passed (607 tests, 1,594 assertions, one skipped). PHPStan and coding standards passed. A selective-run authentication failure was traced to a pre-existing test setup issue: an earlier page query leaked into REQUEST_URI and thus into the signed request-target. The signature test now supplies its actual inbox URI for dispatch and restores server state; production authentication is unchanged.

No additional blocker found in the whole-PR review. The description and testing instructions have been updated.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Security-sensitive sanitization, rendering, and caching changes need human verification, and attachment URL validation issues remain unresolved.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
Resolved since last review (1)

Comment thread includes/collection/class-remote-posts.php Outdated
Comment thread includes/handler/class-create.php Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Explicitly malformed attachment types are classified as images and pass image-only Create validation.

Review effort: Balanced
Findings: None

Resolved since last review (2)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Reject explicitly invalid attachment types instead of defaulting to image

includes/​collection/​class-remote-posts.php:441

When mediaType is absent, an explicitly non-string type falls back to image. A reply with no content and attachment: {"type": 42, "url": "https://example.com/photo.jpg"} therefore passes the new image-only Create validation and gets an image block, even though its attachment type is invalid. Keep the image default for omitted types only; classify explicit invalid types as unsupported. Add regression cases for the extractor and Create validator.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Shared sanitization and block reconstruction warrant human security review, and attachment-validation issues remain unresolved.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
Resolved since last review (1)

Comment thread includes/collection/class-remote-posts.php Outdated
Comment thread includes/collection/class-remote-posts.php
@github-actions github-actions Bot added the Docs label Oct 8, 2026
@pfefferle
pfefferle changed the base branch from trunk to fix/wordpress-html-sanitization October 8, 2026 12:40
@pfefferle
pfefferle changed the base branch from fix/wordpress-html-sanitization to trunk October 9, 2026 10:07
@pfefferle pfefferle closed this Oct 9, 2026
@pfefferle pfefferle reopened this Oct 9, 2026
@pfefferle pfefferle mentioned this pull request Oct 9, 2026
3 of 11 tasks

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Federated comments: images attached to mention are not saved in local comment

2 participants