Run Plugin Check in CI, clear its 70 errors, and guard outbound HTTP statically (P0-4, P0-10) - #8
Open
Shubochandrosarker wants to merge 3 commits into
Open
Conversation
The plugin-check job has been red since it was introduced. It is blocking by design — the point was to see the real number, not to hide it — but the number never came down. This clears every error it reports. No warnings are silenced and no check is downgraded. i18n (44). Forty-one __()/esc_html__()/_n() calls with placeholders had no translators comment, and three strings used bare %s/%d twice, which leaves a translator unable to reorder them for languages that need different word order. The comments say what each placeholder actually is, because "%s: string" would satisfy the sniff and help nobody. Five of the corporate comments had to be wrapped in <?php ?>. They sit in inline HTML, where a bare /* ... */ line is not a comment at all — it is text, and it would have rendered on the group admin screens. Caught by tokenising the file and asserting every translators comment is a real T_COMMENT rather than by reading the diff. Escaping (15). The ten in account.php are the CSS prefix, already reduced to [a-z0-9_-] fifteen lines above the output; esc_attr() is a no-op on that value and is added so the escaping is visible where the output happens. The other five are real: two unescaped counts in a corporate notice, and three unescaped wp_die() arguments in the Stripe service, including a link_url. Filesystem (10). Two of these were already decided and merely mis-suppressed: the readfile() calls carried phpcs:ignore comments naming file_system_read_readfile, but WPCS renamed the sniff to file_system_operations_readfile, so the suppressions matched nothing. The sibling call in class-documents.php uses the current code and was never reported, which is what gave the game away. The rest are php://output export streams — the HTTP response body, not a file — and two streaming CSV readers. WP_Filesystem has no streaming API and no CSV parser, and get_contents_array() splits on newlines, which corrupts any quoted field containing one; member notes and waiver text both do. Suppressed per line with the reason, the same way the ~50 UnescapedDBParameter cases already are. Heredoc (1). admin_css() returned a 79-line CSS heredoc. The body contains no $, no backslash and no single quote, so swapping the delimiters for a single-quoted string needs no escaping and loses no interpolation. The result is "\n" . old . "\n" — a quoted string includes the newlines adjacent to its quotes where a heredoc excludes the one before its terminator — verified by hashing both. It is enqueued as an inline stylesheet, where leading and trailing whitespace is inert. Verification is CI: Plugin Check needs a real WordPress and cannot run in this environment. Lint is clean on all 96 files. The unit suite is 51/839 with the one pre-existing Windows-only DependencyManifestTest path failure, which is fixed separately in #13 and passes on Linux. Backlog stays unticked until the job is actually green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tically Three things this tree is missing, and they belong together: without the job the fixes cannot be verified here, and the guard is the static half of the same network-silence promise the fixes touch. The plugin-check job. P0-4's first acceptance criterion. Blocking, because a listing gate that shows green while the reviewer's own tool reports errors is worse than no gate. It checks the distributable tree and builds it with bin/build-dist.sh — the same script dist-guard and the release workflow run, so the check sees what a reviewer downloads rather than tests/, bin/ and docs/strategy/. Nothing sets ignore-warnings or ignore-errors; the ignore-codes list names specific sniffs and each entry carries its reason inline. The errors. 70 of them on the working copy's equivalent tree: 44 i18n (41 missing translators comments, 3 unordered placeholders), 15 escaping, 10 filesystem, 1 heredoc. Two were not what they looked like. Two readfile() calls already carried phpcs:ignore comments naming file_system_read_readfile, but WPCS renamed the sniff to file_system_operations_readfile, so the suppressions matched nothing — the sibling call in class-documents.php uses the current code and was never reported, which is what exposed it. A stale sniff code is a silent suppression failure. And ten of the fifteen escaping findings were the booking CSS prefix, already reduced to [a-z0-9_-] fifteen lines above the output; esc_attr() there is a no-op, added so the escaping is visible where the output happens. Five were real, including three unescaped wp_die() arguments in the Stripe service. The guard. OutboundHttpAllowlistTest, P0-10's third criterion. FreshInstallTest already proves activation and init reach the network zero times, but only for the paths those two moments execute; a wp_remote_post() added behind admin_init or a cron handler leaves it green. So the files allowed to call out are allow-listed against the integration that gates them, checked in both directions, and matched with token_get_all() rather than a regex — a regex for wp_remote_post also matches the phpdoc describing it. Verified here: lint clean, unit suite 56 tests / 856 assertions green with the new guard against this tree. Plugin Check itself needs a real WordPress and is verified by the job this commit adds. The backlog stays untouched until that job is green on this repository, per the provenance rule on that page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The provenance rule on this page says a box is ticked only when the evidence is here and its CI is green, so these were held back until the job this branch adds had actually reported. Plugin Check: 0 errors on run 31283591069, down from 70 on its first run against this tree. Warnings stood at 270 on that same run, which is the evidence the errors were fixed rather than reclassified into a lower severity or hidden in ignore-codes. The warnings criterion stays open with that count and the two dominant codes named. P0-10 is complete here: the runtime half was already present in FreshInstallTest, and this branch adds the static half. Also records the two findings worth carrying: a phpcs:ignore naming a renamed sniff fails silently, and inserting comments by line number can drop them into inline HTML where they render as text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Three things this tree is missing, and they belong together: without the CI job the fixes cannot be verified here, and the guard is the static half of the same network-silence promise the fixes touch.
plugin-checkjob — P0-4's first acceptance criterion. Blocking, checking the distributable tree built withbin/build-dist.sh.OutboundHttpAllowlistTest— P0-10's third criterion, the static half of invariant I5.Replaces #7, which I opened and closed: I built that on a stale view of
mainfrom the start of the session, before #3–#6 landed here, so it duplicated the convergence through a divergent history and conflicted. This one branches from currentmainand contains only what is genuinely absent.Advances P0-4 and P0-10.
Type
Why these three together
They are one outcome — "Plugin Check runs here and passes" — split across a job, the fixes it demands, and a guard for the invariant two of those fixes touch. Landing the job alone turns CI red on
main; landing the fixes alone leaves them unverified in this repository, which is exactly what the provenance rule on the backlog page warns against. Happy to split if you would rather review them separately.The 70, by category
i18n — 44. Forty-one
__()/esc_html__()/_n()calls with placeholders had notranslators:comment, and three strings used bare%s/%dtwice, leaving a translator unable to reorder them for languages that need different word order. The comments say what each placeholder actually is —%s: stringwould satisfy the sniff and help nobody.Escaping — 15. Ten are the booking CSS prefix in
templates/account.php, already reduced to[a-z0-9_-]by apreg_replace()fifteen lines above the output.esc_attr()is a genuine no-op on that value; it is added so the escaping is visible where the output happens, with a comment recording why. Five are real: two unescaped counts in a corporate admin notice, and three unescapedwp_die()arguments in the Stripe service, including alink_url.Filesystem — 10. Two were already decided and merely mis-suppressed: the
readfile()calls carriedphpcs:ignorecomments namingfile_system_read_readfile, but WPCS renamed the sniff tofile_system_operations_readfile, so they matched nothing and the calls were reported anyway. The sibling inclass-documents.phpuses the current code and was never reported — that mismatch is what exposed it. A stale sniff code is a silent suppression failure; nothing warns you. The other eight are twophp://outputexport streams (the HTTP response body, not a file) and two streaming CSV readers.WP_Filesystemhas no streaming API and no CSV parser, andget_contents_array()splits on newlines, which corrupts any quoted field containing one — member notes and waiver text both do. Suppressed per line with the reason, the way the ~50UnescapedDBParametercases already are.Heredoc — 1.
admin_css()returned a 79-line CSS heredoc. The body has no$, backslash or single quote, so the delimiter swap needs no escaping.One deliberate, verified behaviour delta
The heredoc conversion is not byte-identical. A quoted string includes the newlines adjacent to its quotes; a heredoc excludes the one before its terminator. Verified by hashing both literals in isolation:
It is enqueued as an inline stylesheet, where leading and trailing whitespace is inert. Flagging it rather than claiming identity.
Rules touched
permission_callbackStrengthened, not relaxed.
FreshInstallTestalready proves activation andinitreach the network zero times, but only for the paths those two moments execute — awp_remote_post()added behindadmin_init, a shortcode, a REST callback or a cron handler leaves that suite green and still breaks the promise. The new guard allow-lists the files permitted to call out against the integration key gating them (two here, coreSTORE and Stripe, both'no'), checks the list in both directions so an entry whose call disappears must be removed rather than left as a standing permission, and asserts the gating toggle ships off rather than merely that the file is listed. Matching istoken_get_all(), not a regex: a regex forwp_remote_postalso matches the phpdoc describing it, which fills the allow-list with files that never call anything and hides the one that does.Multi-edit checklist
MEMBERISTIC_DB_VERSIONuntouched, no version bump: this is not a release PR.Documentation
CHANGELOG.mdandreadme.txtare untouched for the same reason.The backlog is deliberately not ticked in this commit. The provenance rule on that page says a box is ticked only when the evidence is in this repository and its CI is green. Plugin Check has never run here, so I cannot honestly tick P0-4 before this PR's own job reports. I will push the backlog update once it is green — or fix whatever it finds first.
Follow-up not included: four translatable strings changed msgid (the placeholder reordering). Only
languages/memberistic.potships — no compiled.po/.mo— so no translation is lost, but the.potis stale for those four entries and lacks the new translator comments. Regenerating needs WP-CLI (wp i18n make-pot).What I ran
Against this tree, not the working copy:
The guard test passing here matters: its allow-list is derived from the working copy, and this confirms this tree has the same two outbound call sites and no others.
Both new guards were mutation-tested in both directions rather than trusted because they were green:
On the mirror, where the job already exists, this same set of fixes took Plugin Check from 70 errors to 0 with warnings unchanged at 266 — that pairing is the evidence the errors were fixed rather than reclassified.
What I did NOT test
ci.ymlYAML parse. Nopython+yamlavailable locally. Structurally checked (job key at the same indentation as its siblings, no tabs), but GitHub is the real parser.<?php ?>in inline HTML, and?>swallows the following newline, so the emitted markup shifts by a newline in five places. Inert in HTML, not eyeballed.Risk and rollback
wp_die()argument escapes, two integer casts, a CSS literal delimiter change, and comment/annotation additions. The realistic failure mode is a display regression on the corporate group screens, not data loss. The one new blocking job could turnmainred if it behaves differently here than on the mirror — that is the point of it, but it is the change most likely to need a follow-up.plugin-checkjob alone if only the gate is unwanted.🤖 Generated with Claude Code