Skip to content

Commit 37bdcdd

Browse files
author
Claude
committed
test: expand attachment redirect tests for parent post visibility
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.
1 parent 3c98e41 commit 37bdcdd

1 file changed

Lines changed: 171 additions & 23 deletions

File tree

tests/phpunit/tests/canonical/attachmentRedirect.php

Lines changed: 171 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,58 +10,206 @@
1010
class Tests_Canonical_AttachmentRedirect extends WP_Canonical_UnitTestCase {
1111

1212
/**
13-
* Attachment post object.
14-
*
15-
* @var WP_Post
13+
* Attachment post objects and related fixtures.
1614
*/
17-
public static $attachment;
15+
public static $unattached;
16+
public static $public_parent;
17+
public static $attached_to_public;
18+
public static $private_parent;
19+
public static $attached_to_private;
20+
public static $draft_parent;
21+
public static $attached_to_draft;
22+
public static $editor_user;
1823

1924
public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
20-
// Create a fake attachment (no real file upload needed).
21-
$attachment_id = $factory->post->create(
25+
self::$editor_user = $factory->user->create( array( 'role' => 'editor' ) );
26+
27+
// Unattached attachment.
28+
$unattached_id = $factory->post->create(
2229
array(
2330
'post_type' => 'attachment',
24-
'post_title' => 'Test Image',
25-
'post_name' => 'test-image-jpg',
31+
'post_title' => 'Unattached Image',
32+
'post_name' => 'unattached-image',
2633
'post_status' => 'inherit',
2734
'post_parent' => 0,
2835
)
2936
);
37+
update_post_meta( $unattached_id, '_wp_attached_file', '2025/01/unattached-image.jpg' );
38+
self::$unattached = get_post( $unattached_id );
39+
40+
// Attachment on a public post.
41+
self::$public_parent = $factory->post->create_and_get(
42+
array(
43+
'post_type' => 'post',
44+
'post_title' => 'Public Post',
45+
'post_name' => 'public-post',
46+
'post_status' => 'publish',
47+
)
48+
);
49+
$attached_public_id = $factory->post->create(
50+
array(
51+
'post_type' => 'attachment',
52+
'post_title' => 'Public Attached Image',
53+
'post_name' => 'public-attached-image',
54+
'post_status' => 'inherit',
55+
'post_parent' => self::$public_parent->ID,
56+
)
57+
);
58+
update_post_meta( $attached_public_id, '_wp_attached_file', '2025/01/public-attached-image.jpg' );
59+
self::$attached_to_public = get_post( $attached_public_id );
60+
61+
// Attachment on a private post.
62+
self::$private_parent = $factory->post->create_and_get(
63+
array(
64+
'post_type' => 'post',
65+
'post_title' => 'Private Post',
66+
'post_name' => 'private-post',
67+
'post_status' => 'private',
68+
'post_author' => self::$editor_user,
69+
)
70+
);
71+
$attached_private_id = $factory->post->create(
72+
array(
73+
'post_type' => 'attachment',
74+
'post_title' => 'Private Attached Image',
75+
'post_name' => 'private-attached-image',
76+
'post_status' => 'inherit',
77+
'post_parent' => self::$private_parent->ID,
78+
)
79+
);
80+
update_post_meta( $attached_private_id, '_wp_attached_file', '2025/01/private-attached-image.jpg' );
81+
self::$attached_to_private = get_post( $attached_private_id );
3082

31-
// Set a fake attachment URL via metadata.
32-
update_post_meta( $attachment_id, '_wp_attached_file', '2025/01/test-image.jpg' );
83+
// Attachment on a draft post.
84+
self::$draft_parent = $factory->post->create_and_get(
85+
array(
86+
'post_type' => 'post',
87+
'post_title' => 'Draft Post',
88+
'post_name' => 'draft-post',
89+
'post_status' => 'draft',
90+
'post_author' => self::$editor_user,
91+
)
92+
);
93+
$attached_draft_id = $factory->post->create(
94+
array(
95+
'post_type' => 'attachment',
96+
'post_title' => 'Draft Attached Image',
97+
'post_name' => 'draft-attached-image',
98+
'post_status' => 'inherit',
99+
'post_parent' => self::$draft_parent->ID,
100+
)
101+
);
102+
update_post_meta( $attached_draft_id, '_wp_attached_file', '2025/01/draft-attached-image.jpg' );
103+
self::$attached_to_draft = get_post( $attached_draft_id );
104+
}
33105

34-
self::$attachment = get_post( $attachment_id );
106+
/**
107+
* Helper to get the expected redirect path for an attachment.
108+
*/
109+
private function get_expected_path( $attachment_id ) {
110+
return parse_url( wp_get_attachment_url( $attachment_id ), PHP_URL_PATH );
35111
}
36112

113+
// -------------------------------------------------------------------------
114+
// Unattached attachment tests.
115+
// -------------------------------------------------------------------------
116+
37117
/**
38118
* Pretty permalink slug-based attachment URLs should redirect to the file URL
39119
* when wp_attachment_pages_enabled is 0.
40120
*
41-
* This is a regression test: get_query_var( 'attachment_id' ) is only populated
42-
* for ?attachment_id=123 URLs, not slug-based URLs. The fix falls back to
43-
* get_queried_object_id().
121+
* This is the primary regression test: get_query_var( 'attachment_id' ) is only
122+
* populated for ?attachment_id=123 URLs, not slug-based URLs. The fix falls back
123+
* to get_queried_object_id().
44124
*/
45-
public function test_pretty_permalink_attachment_redirects_when_pages_disabled() {
125+
public function test_unattached_slug_url_redirects_when_pages_disabled() {
46126
update_option( 'wp_attachment_pages_enabled', 0 );
47127
$this->set_permalink_structure( '/%postname%/' );
48128

49-
// assertCanonical compares against the path component only.
50-
$expected_path = parse_url( wp_get_attachment_url( self::$attachment->ID ), PHP_URL_PATH );
51-
52-
$this->assertCanonical( '/test-image-jpg/', $expected_path );
129+
$this->assertCanonical( '/unattached-image/', $this->get_expected_path( self::$unattached->ID ) );
53130
}
54131

55132
/**
56133
* Query string ?attachment_id=ID should also redirect when pages are disabled.
57134
*/
58-
public function test_query_var_attachment_redirects_when_pages_disabled() {
135+
public function test_unattached_query_var_url_redirects_when_pages_disabled() {
59136
update_option( 'wp_attachment_pages_enabled', 0 );
60137
$this->set_permalink_structure( '/%postname%/' );
61138

62-
// assertCanonical compares against the path component only.
63-
$expected_path = parse_url( wp_get_attachment_url( self::$attachment->ID ), PHP_URL_PATH );
139+
$this->assertCanonical( '/?attachment_id=' . self::$unattached->ID, $this->get_expected_path( self::$unattached->ID ) );
140+
}
141+
142+
// -------------------------------------------------------------------------
143+
// Attached to a public post.
144+
// -------------------------------------------------------------------------
145+
146+
/**
147+
* Attachment on a public post should redirect via slug URL.
148+
*/
149+
public function test_attached_to_public_post_slug_url_redirects() {
150+
update_option( 'wp_attachment_pages_enabled', 0 );
151+
$this->set_permalink_structure( '/%postname%/' );
152+
153+
$this->assertCanonical( '/public-attached-image/', $this->get_expected_path( self::$attached_to_public->ID ) );
154+
}
155+
156+
// -------------------------------------------------------------------------
157+
// Attached to a private post — logged out (should NOT redirect).
158+
// -------------------------------------------------------------------------
159+
160+
/**
161+
* Attachment on a private post should not redirect for anonymous users,
162+
* to avoid leaking the file URL.
163+
*/
164+
public function test_attached_to_private_post_no_redirect_for_anonymous() {
165+
update_option( 'wp_attachment_pages_enabled', 0 );
166+
$this->set_permalink_structure( '/%postname%/' );
167+
wp_set_current_user( 0 );
168+
169+
$this->assertCanonical( '/private-attached-image/', '/private-attached-image/' );
170+
}
171+
172+
// -------------------------------------------------------------------------
173+
// Attached to a private post — authorized user (should redirect).
174+
// -------------------------------------------------------------------------
175+
176+
/**
177+
* Attachment on a private post should redirect for a user who can read it.
178+
*/
179+
public function test_attached_to_private_post_redirects_for_authorized_user() {
180+
update_option( 'wp_attachment_pages_enabled', 0 );
181+
$this->set_permalink_structure( '/%postname%/' );
182+
wp_set_current_user( self::$editor_user );
183+
184+
$this->assertCanonical( '/private-attached-image/', $this->get_expected_path( self::$attached_to_private->ID ) );
185+
}
186+
187+
// -------------------------------------------------------------------------
188+
// Attached to a draft post (should NOT redirect).
189+
// -------------------------------------------------------------------------
190+
191+
/**
192+
* Attachment on a draft post should not redirect for anonymous users.
193+
*/
194+
public function test_attached_to_draft_post_no_redirect_for_anonymous() {
195+
update_option( 'wp_attachment_pages_enabled', 0 );
196+
$this->set_permalink_structure( '/%postname%/' );
197+
wp_set_current_user( 0 );
198+
199+
$this->assertCanonical( '/draft-attached-image/', '/draft-attached-image/' );
200+
}
201+
202+
// -------------------------------------------------------------------------
203+
// Pages enabled — should NOT redirect.
204+
// -------------------------------------------------------------------------
205+
206+
/**
207+
* When attachment pages are enabled, slug URLs should not redirect to the file.
208+
*/
209+
public function test_no_redirect_when_attachment_pages_enabled() {
210+
update_option( 'wp_attachment_pages_enabled', 1 );
211+
$this->set_permalink_structure( '/%postname%/' );
64212

65-
$this->assertCanonical( '/?attachment_id=' . self::$attachment->ID, $expected_path );
213+
$this->assertCanonical( '/unattached-image/', '/unattached-image/' );
66214
}
67215
}

0 commit comments

Comments
 (0)