Skip to content

Commit 6042b5f

Browse files
KokkieHclaude
andauthored
Sponsors: store agreement attachments with the private status (#1992)
- Name, mark and set `private` on an agreement in the request that uploads it; a PDF uploaded against a sponsor counts without the mark - Mark, set `private` and rename an agreement recorded from the Media Library when the sponsor is saved - Leave marked agreements out of the XML-RPC and admin-ajax media details for users who can't read them - Record an agreement whose file still needs renaming, and log an upload that reaches a save unmarked - Have the Sponsor Agreement modal mark its own uploads - Add a one-time `wp wc-sponsor-agreements` migration for agreements attached before this, to be removed once it has been run Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 810e991 commit 6042b5f

8 files changed

Lines changed: 2256 additions & 1 deletion

File tree

‎public_html/wp-content/mu-plugins/sponsor-agreements.php‎

Lines changed: 592 additions & 0 deletions
Large diffs are not rendered by default.

‎public_html/wp-content/mu-plugins/tests/bootstrap.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ function manually_load_plugins() {
3838
require_once dirname( __DIR__ ) . '/wcorg-network-theme-control.php';
3939
require_once dirname( __DIR__ ) . '/jetpack-tweaks/import-meta.php';
4040
require_once dirname( __DIR__ ) . '/importer-tweaks.php';
41+
require_once dirname( __DIR__ ) . '/sponsor-agreements.php';
4142
}
4243

4344
tests_add_filter( 'muplugins_loaded', __NAMESPACE__ . '\manually_load_plugins' );
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<?php
2+
3+
namespace WordCamp\Sponsor_Agreements\Backfill\Tests;
4+
5+
use WP_UnitTestCase;
6+
7+
use function WordCamp\Sponsor_Agreements\Backfill\get_public_agreement_ids;
8+
use function WordCamp\Sponsor_Agreements\is_agreement;
9+
use function WordCamp\Sponsor_Agreements\make_agreement_private;
10+
11+
use const WordCamp\Sponsor_Agreements\AGREEMENT_MARKER_META_KEY;
12+
use const WordCamp\Sponsor_Agreements\NEEDS_RENAME_META_KEY;
13+
14+
defined( 'WPINC' ) || die();
15+
16+
/**
17+
* The one-time migration behind `wp wc-sponsor-agreements`.
18+
*
19+
* Goes when `wp-cli-commands/backfill-sponsor-agreements.php` does. `Test_Sponsor_Agreements` covers what
20+
* stays.
21+
*
22+
* @group mu-plugins
23+
* @group sponsor-agreements
24+
*/
25+
class Test_Backfill_Sponsor_Agreements extends WP_UnitTestCase {
26+
/**
27+
* The command file only loads under WP-CLI, so pull in the part of it that isn't the command.
28+
*/
29+
public static function set_up_before_class() {
30+
parent::set_up_before_class();
31+
32+
require_once dirname( __DIR__ ) . '/wp-cli-commands/backfill-sponsor-agreements.php';
33+
}
34+
35+
/**
36+
* Register the sponsor post type, which lives in a plugin this suite doesn't load.
37+
*
38+
* No matching `tear_down()`: `WP_UnitTestCase` resets the registered post types itself before each
39+
* test, and unregistering by hand here takes rewrite rules and registered meta with it.
40+
*/
41+
public function set_up() {
42+
parent::set_up();
43+
44+
register_post_type( 'wcb_sponsor', array( 'public' => true ) );
45+
}
46+
47+
/**
48+
* Attach a file to a sponsor and name it as the agreement, without the meta hook seeing it.
49+
*
50+
* This is the shape of a row written before `sponsor-agreements.php` existed, and the only shape the
51+
* migration has to deal with.
52+
*
53+
* @param string $filename
54+
*
55+
* @return array The sponsor ID and the attachment ID.
56+
*/
57+
protected function create_legacy_agreement( $filename = 'sponsorship-agreement-acme-signed.pdf' ) {
58+
$sponsor_id = self::factory()->post->create( array(
59+
'post_type' => 'wcb_sponsor',
60+
'post_status' => 'publish',
61+
) );
62+
63+
$agreement_id = self::factory()->attachment->create_object( array(
64+
'file' => $filename,
65+
'post_parent' => $sponsor_id,
66+
'post_status' => 'inherit',
67+
'post_mime_type' => 'application/pdf',
68+
) );
69+
70+
add_post_meta( $sponsor_id, '_wcpt_sponsor_agreement', $agreement_id );
71+
wp_update_post( array(
72+
'ID' => $agreement_id,
73+
'post_status' => 'inherit',
74+
) );
75+
76+
return array( $sponsor_id, $agreement_id );
77+
}
78+
79+
/**
80+
* The migration finds the files it's for, and nothing else.
81+
*/
82+
public function test_legacy_agreements_are_reported_and_migrated() {
83+
list( $sponsor_id, $agreement_id ) = $this->create_legacy_agreement();
84+
85+
$logo_id = self::factory()->attachment->create_object( array(
86+
'file' => 'acme-logo.png',
87+
'post_parent' => $sponsor_id,
88+
'post_status' => 'inherit',
89+
'post_mime_type' => 'image/png',
90+
) );
91+
92+
$public = get_public_agreement_ids();
93+
94+
$this->assertContains( $agreement_id, $public );
95+
$this->assertNotContains( $logo_id, $public, 'A sponsor logo is meant to be public.' );
96+
97+
$this->assertTrue( make_agreement_private( $agreement_id ) );
98+
$this->assertSame( 'private', get_post_status( $agreement_id ) );
99+
$this->assertTrue( is_agreement( $agreement_id ) );
100+
$this->assertNotContains( $agreement_id, get_public_agreement_ids() );
101+
}
102+
103+
/**
104+
* Naming a site explicitly is what lets `scan` ask this of a whole network from one process.
105+
*/
106+
public function test_the_query_can_be_asked_of_a_named_site() {
107+
list( , $agreement_id ) = $this->create_legacy_agreement();
108+
109+
$this->assertContains( $agreement_id, get_public_agreement_ids( get_current_blog_id() ) );
110+
}
111+
112+
/**
113+
* The mark stays on the attachment after the migration has changed its status.
114+
*/
115+
public function test_a_migrated_agreement_stays_findable_by_its_mark() {
116+
list( , $agreement_id ) = $this->create_legacy_agreement();
117+
118+
make_agreement_private( $agreement_id );
119+
120+
$marked = get_posts( array(
121+
'post_type' => 'attachment',
122+
'post_status' => 'any',
123+
'fields' => 'ids',
124+
'numberposts' => -1,
125+
'meta_key' => AGREEMENT_MARKER_META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- a test fixture, on a table with a handful of rows.
126+
) );
127+
128+
$this->assertContains( $agreement_id, array_map( 'intval', $marked ) );
129+
}
130+
131+
/**
132+
* Running twice does nothing the second time.
133+
*/
134+
public function test_the_migration_is_safe_to_re_run() {
135+
list( , $agreement_id ) = $this->create_legacy_agreement();
136+
137+
$this->assertTrue( make_agreement_private( $agreement_id ) );
138+
$this->assertSame( array(), get_public_agreement_ids() );
139+
$this->assertFalse( make_agreement_private( $agreement_id ), 'A second pass reported a change it did not make.' );
140+
$this->assertSame( 'private', get_post_status( $agreement_id ) );
141+
}
142+
143+
/**
144+
* The migration records that the files it acted on still need renaming.
145+
*
146+
* Nothing else says so once they are no longer `inherit`, and it is what a later rename pass reads.
147+
*/
148+
public function test_the_migration_records_what_still_needs_renaming() {
149+
list( , $legacy_id ) = $this->create_legacy_agreement();
150+
151+
$sponsor_id = self::factory()->post->create( array(
152+
'post_type' => 'wcb_sponsor',
153+
'post_status' => 'publish',
154+
) );
155+
$uploads = wp_upload_dir();
156+
$directory = trailingslashit( $uploads['path'] );
157+
158+
// A real file: the hook renames on record, and an attachment with no file is itself recorded.
159+
file_put_contents( $directory . 'recent-agreement.pdf', 'x' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- a fixture on the local disk.
160+
161+
$recent_id = self::factory()->attachment->create_object( array(
162+
'file' => $directory . 'recent-agreement.pdf',
163+
'post_parent' => $sponsor_id,
164+
'post_status' => 'inherit',
165+
'post_mime_type' => 'application/pdf',
166+
) );
167+
168+
update_attached_file( $recent_id, $directory . 'recent-agreement.pdf' );
169+
170+
try {
171+
// The hook covers this one as it's attached, so the migration never sees it.
172+
update_post_meta( $sponsor_id, '_wcpt_sponsor_agreement', $recent_id );
173+
174+
$this->assertContains( $legacy_id, get_public_agreement_ids() );
175+
$this->assertNotContains( $recent_id, get_public_agreement_ids() );
176+
177+
// What `Command::backfill()` does for each ID the query returns.
178+
make_agreement_private( $legacy_id );
179+
update_post_meta( $legacy_id, NEEDS_RENAME_META_KEY, 1 );
180+
181+
$this->assertTrue( is_agreement( $legacy_id ) );
182+
$this->assertTrue( is_agreement( $recent_id ) );
183+
184+
$this->assertSame( '1', get_post_meta( $legacy_id, NEEDS_RENAME_META_KEY, true ) );
185+
$this->assertSame( '', get_post_meta( $recent_id, NEEDS_RENAME_META_KEY, true ) );
186+
} finally {
187+
foreach ( glob( $directory . 'recent-agreement*' ) as $path ) {
188+
wp_delete_file( $path );
189+
}
190+
}
191+
}
192+
}

0 commit comments

Comments
 (0)