Repository navigation
Budgets: store payment files with the private status and delete them with their request - #2086
Conversation
afdaecf to
d148fe2
Compare
|
Reviewed over four passes, latest at 1acf6b5. Everything I raised is addressed, several of them better than what I suggested. Detailed notes are in the private discussion where this work is tracked. First pass. Files stored Second pass. Asking for Third pass. All three closed. The two result paths answer the same question, with a test that drives it through a real Fourth pass. That one is fixed, and the capture condition now matches No further findings. Approving. I have not run the suite at any point, so the tests passing is your claim rather than something I checked. |
|
Thanks. Two commits with the changes are up. This is ready for another look. |
A reimbursement or vendor payment request's attachments are only ever read by the Files metabox on that request, but `wp_delete_post()` leaves them behind, reparented to the deleted post's own parent. Delete them with the request instead, row and file. This runs on `before_delete_post`, so it covers the Delete Permanently action along with the scheduled purges of trashed posts and of abandoned auto-drafts. Trashing still keeps everything, so restoring a request brings its files back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A file uploaded onto a reimbursement or vendor payment request now carries its own marker and the `private` status from the moment it is created, and files attached later, through the Files metabox or the Media Library's Attach action, are marked as they are reparented. The rules that decide who reads a payment file read that marker first, and still fall back to the parent for files created before this change. `read_post` is now decided alongside `edit_post`, because `private` maps it to a capability every organizer has, and the media details the admin hands to JavaScript are redacted the same way the XML-RPC ones already were. A file's classification shouldn't depend on a parent that can be deleted or changed after the fact. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`wp wc-budget-files` gives the marker and the `private` status to files that were uploaded onto a reimbursement or vendor payment request before `privacy.php` started applying them, and to files with no parent whose name still identifies them as one. It writes meta and post status and nothing else, one site per run, with `scan` reading the network to say which sites need a run and which files are copies of one still sitting on a request elsewhere. The two new meta keys are the work lists a later step reads, so they stay in `privacy.php` when the command is deleted. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`Database_TestCase` truncates `sitemeta` to build the WordCamp networks, so `check_upload_mimes()` has no `upload_filetypes` to read and falls back to images only for the rest of the run. The fixtures that write a real file now allow PDFs, which production does, and fail loudly if the upload does not happen. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…scan honest Core unlinks an attachment's file with @Unlink() and reports nothing, so a deletion run where uploads aren't writable removes the row and leaves the file. The cascade now logs budget_file_not_deleted when that happens. The backfill scan checks for a query error after each of its two queries rather than once after both, and skips deleted and archived sites. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The unattached arm was reported whole, marked files included, so a site never left the scan after a clean run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The status filter now acts on inserts only, so an unrelated attachment saved while a request's edit screen is open keeps its status. The marker is read by existence, matching the SQL guard, the upload helper ignores a non-scalar post parameter, and result priming now includes meta so the marker check doesn't query per attachment. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ackfill's copy list on every run The Media Library screens append `private` only for `read_private_posts`, which an Author lacks, so a requester who is a volunteer saw none of their own files. They ask for the status now, and the SQL guard keeps the gate those screens skip, so nothing else private comes with it. The parent lookup reads the post cache instead of querying, so a trashed or unsaved request still counts, and attaching a file only marks one that had no parent before the attach. The backfill records a copy on every run its ID is named in, marked or not, since a copy can be marked before the scan finds its original elsewhere. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…kup from the admin-ajax redaction `wp_insert_attachment()` called from PHP sends none of the parameters the upload routes do, so the marker and the status are now written whenever the inserted row's own parent is a request. The fixtures that model rows from before any of this drop the hook while they insert. The redaction the admin hands to JavaScript reaches the same two cache-only early-outs the capability filter has, now that both share them, and the status write happens once per call chain rather than once per hook it triggers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…revious parent before Core writes the new one The pass over a query's results dropped only payment files, so a set supplied through `posts_pre_query` came back wider than the same query's clauses would have allowed. It now drops the same set, and both paths read the capability off the attachment post type rather than naming it. The Media Library's Attach action writes `post_parent` with a direct `UPDATE`, so the previous value is now captured on `load-upload.php` instead of read back out of the post cache, and a file nobody captured is left alone. `scan` also keeps a site that still has copies to record, which it dropped once nothing was left to mark. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
61eed48 to
0279e9f
Compare
`wp_media_attach_action()` casts `media` to an array, so a plain `?action=attach&media=123` attaches one file. The screen-load capture skipped that shape and left the file unmarked. Cast the same way. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Thanks. One more commit with the cast is up. |
bor0
left a comment
There was a problem hiding this comment.
Approving at 1acf6b5. Everything I raised across the four passes is closed; my notes are in the comment above and in the private discussion where this work is tracked.
I have not run the suite, so the tests passing is your claim rather than something I checked.
Why
A budget request's files (the invoices and receipts on a reimbursement or vendor payment request) are classified by their parent.
privacy.phpdecides who may see a file by resolving itspost_parentto a live request, and the attachment itself is stored asinherit, like any other upload. So the file's classification lives on a different row than the file, and it holds only as long as that row does.wp_delete_post()doesn't delete a post's attachments, it points them at the deleted post's own parent, which is0for these types, and from then on they are ordinary unattached media. That happens more often than a manual delete suggests: Core purgesauto-draftposts older than 7 days, and the Files metabox uploads against the auto-draft the Add New screen opens with, so a request that is never saved leaves its files behind a week later. Cloning a site for a new year copies the media library without the requests and does the same thing in bulk. A census from the sandbox confirmed both shapes exist on the network.The fix is the same one #1992 applies to sponsor agreements: record what a file is on the file itself, as it is created, and store it
privateso it doesn't inherit whatever its parent happens to be. The parent rule stays only for files created before this change, until a one-time backfill has marked them.Two smaller things in the same code. Files on an
auto-draftrequest are missed by the second-pass rules, becauseget_payment_file_parent_ids()looks requests up withpost_status => 'any', which excludesauto-draft. Andwp_ajax_get_attachment()resolves by ID and checksupload_filesalone, so the media details it hands to JavaScript were never run through the rules the XML-RPC ones already were.What changed
before_delete_poston both request types, so Delete Permanently, the trash purge and the auto-draft purge all take the files. Trashing keeps them, and restoring brings them back attached.private, with a_wcorg_budget_filemarker and the random suffix, in the request that creates it (wp_unique_filename,wp_insert_attachment_data,add_attachment). Anauto-draftrequest counts.privacy.phpreads the marker first and falls back to the parent for files created before this change.read_postis decided alongsideedit_post, sinceprivatemaps it toread_private_posts, which every organizer has. The readers are unchanged: uploader, requester, site admin.wp_prepare_attachment_for_jsruns through the same redaction asxmlrpc_prepare_media_item.privateupload on their request (WP_Querywould otherwise gate it onread_private_posts).exclude_others_payment_files()is twoLEFT JOINs instead of a correlatedNOT INsubquery.wp wc-budget-files scanandbackfill, gives existing files the marker and the status. Database only, one site per run, safe to re-run.scanwalkswp_blogsby table prefix without loading sites and prints a ready-to-runbackfillline per site, with the copies it matched across sites.backfillwrites_wcorg_budget_file_backfilledon each file and_wcorg_budget_file_deleteon copies, as work lists for later steps; it deletes nothing. The command and its tests get removed after the run.Nothing here renames or moves a file, and nothing touches the sponsor agreement work in #1992.
Testing
20 new tests in
--group budgets, 60 in the group, all passing. The three cascade paths (manual delete, trash purge, auto-draft purge), both attach routes and both upload cases fail againstproductionand pass with the change; trash-and-restore and "an ordinary post is left as Core leaves it" pass both ways by design.Compatibility: an upload against an ordinary post is untouched (
inherit, no marker, no suffix). A co-organizer's upload on an Author's request stays visible to that Author and the submit check still passes. A file from before this change,inheritand unmarked, is still scoped by its parent. Deleting a request as a co-organizer who can't see another organizer's upload on it still takes that upload.Deployment:
scanandbackfill --dry-runrun on the sandbox before merge, read-only. After deploy,backfillonce per listed site (central needs--yes-main-site), then the command gets deleted.One thing found on the way and left alone: an organizer can't trash their own draft request, because
set_request_status()clampstrashout of the requester status set. The trash tests run as a network admin for that reason. Separate ticket.🤖 Generated with Claude Code