Skip to content

Media: Redirect slug-based attachment URLs when attachment pages are disabled#11816

Open
dd32 wants to merge 6 commits into
WordPress:trunkfrom
dd32:fix/attachment-redirect-pretty-permalinks
Open

Media: Redirect slug-based attachment URLs when attachment pages are disabled#11816
dd32 wants to merge 6 commits into
WordPress:trunkfrom
dd32:fix/attachment-redirect-pretty-permalinks

Conversation

@dd32
Copy link
Copy Markdown
Member

@dd32 dd32 commented May 13, 2026

When wp_attachment_pages_enabled is 0 and pretty permalinks are active, accessing an attachment via its slug URL (e.g. /my-image-jpg/) currently returns a 200 with an attachment page rendered, instead of redirecting to the underlying file. The query-string variant (?attachment_id=123) for the same attachment correctly redirects.

The cause is in redirect_canonical() (src/wp-includes/canonical.php around line 553): the code retrieves the attachment ID via get_query_var( attachment_id ), which is only populated for ?attachment_id=N URLs. For slug-based attachment URLs the query var is empty, so get_post() receives an empty value, returns null, and the redirect is silently skipped.

This change falls back to get_queried_object_id() when the attachment_id query var is empty, so the attachment ID resolves correctly for both URL forms.

Trac ticket: https://core.trac.wordpress.org/ticket/65230

Note: This PR was prepared with the assistance of AI (Claude). The analysis, code, and tests below were reviewed by the submitter before opening.

Root Cause Analysis

The bug originates from the initial implementation of wp_attachment_pages_enabled in r56657 (WordPress 6.4, Trac #57913). The redirect logic has always used get_query_var( attachment_id ) to resolve the attachment, which is only populated for ?attachment_id=123 URLs — never for slug-based pretty permalink URLs like /my-image-jpg/.

Trac History

#57913 — The original ticket's intent was unambiguous: "setting [the toggle] to off should disable the attachment URLs entirely, redirecting existing attachment URLs to their parent post." The announcement post described the feature as "attachment pages for new WordPress installations are fully disabled."

#59866 — The follow-up ticket reported that the redirect didn't work for logged-out users (due to a current_user_can check). During testing, @joppuyo explicitly demonstrated in comment:3 that slug-based URLs also don't redirect:

Upload image foo.jpeg to the media gallery
Visit https://example.com/foo as a logged-in user
Attachment page is displayed

They noted:

So instead of "fully disabling" the attachment pages current functionality could be described as "redirects attachment pages to the file if you are logged in" which feels a bit half baked.

In comment:5, @afercia acknowledged the slug-based case but expressed uncertainty about whether it was in scope:

To my understanding, your reproduction steps try to access the attachment page url directly? Im not sure thats a case that has been covered by the current implementation neither Im sure whether it should be covered.

@chesio pushed back in comment:6:

I dont think its safe to assume that attachment page URLs can be only discovered for attachments attached to a post. Therefore I would prefer this feature to cover all attachments.

The fix that landed in r57357 removed the current_user_can gate (fixing the logged-out user issue) but kept the same get_query_var( attachment_id ) call, leaving slug-based URLs still broken.

This PR

Falls back to get_queried_object_id() when get_query_var( attachment_id ) is empty. WordPress already resolves the attachment via the slug rewrite rule and sets the queried object — we just weren't reading it.

Test Coverage

Expanded the test file to cover the parent-post visibility guard at canonical.php line ~800. When an attachment is attached to a post, $redirect_obj is set to the parent post, and the later visibility check determines whether to allow or suppress the redirect.

Test Scenario Expected
test_unattached_slug_url_redirects_when_pages_disabled Unattached attachment, slug URL /unattached-image/ Redirect to file URL
test_unattached_query_var_url_redirects_when_pages_disabled Unattached attachment, ?attachment_id=ID Redirect to file URL
test_attached_to_public_post_slug_url_redirects Attached to published post, slug URL Redirect to file URL
test_attached_to_private_post_no_redirect_for_anonymous Attached to private post, anonymous user No redirect (prevents leaking file URL)
test_attached_to_private_post_redirects_for_authorized_user Attached to private post, editor user Redirect to file URL
test_attached_to_draft_post_no_redirect_for_anonymous Attached to draft post, anonymous user No redirect
test_no_redirect_when_attachment_pages_enabled wp_attachment_pages_enabled = 1 No redirect (feature off)

The first two tests are the core regression tests for the get_query_var(attachment_id) fix. The remaining five verify that the existing privacy logic (lines 800-820 of canonical.php) works correctly now that $attachment_id is properly resolved for slug-based URLs — particularly that we don't accidentally leak file URLs for attachments on private or draft posts.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

Claude added 5 commits April 9, 2026 13:07
redirect_canonical() fails to redirect attachment pages accessed via
pretty permalinks (e.g. /my-image-jpg/) when wp_attachment_pages_enabled
is 0. get_query_var( 'attachment_id' ) is only populated for
?attachment_id=123 URLs, not slug-based URLs.

Falls back to get_queried_object_id() when the query var is empty.
assertCanonical() compares against the path component of the redirect
URL, not the full URL. Use parse_url() to extract the path from
wp_get_attachment_url().
Cover the privacy guard at canonical.php line ~800: attachments on
private posts should not redirect for anonymous users (would leak the
file URL), but should redirect for authorized users. Also cover
attachments on draft posts and the pages-enabled no-redirect case.
Use create_and_get() directly into self:: properties to avoid
intermediate variables that trigger MultipleStatementAlignment warnings.
Child attachment URLs with pretty permalinks take the form
/parent-slug/attachment-slug/, not just /attachment-slug/. Fix test URLs
to include the parent post slug.

Break consecutive self:: assignments with blank lines to avoid PHPCS
MultipleStatementAlignment warnings between differently-named properties.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 13, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @claude@local.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

Core Committers: Use this line as a base for the props when committing in SVN:

Props dd32.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

* populated for ?attachment_id=123 URLs, not slug-based URLs. The fix falls back
* to get_queried_object_id().
*/
public function test_unattached_slug_url_redirects_when_pages_disabled() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add @ticket annotation for all the newly added tests

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants