Skip to content

Fix 2.0.0 audit findings + pin generated plugins to PHP 8.3 - #10

Merged
akshat009 merged 12 commits into
mainfrom
fix/audit-2.0.0
Aug 29, 2026
Merged

Fix 2.0.0 audit findings + pin generated plugins to PHP 8.3#10
akshat009 merged 12 commits into
mainfrom
fix/audit-2.0.0

Conversation

@akshat009

@akshat009akshat009 commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Addresses the external audit of create-wp-plugin-cli@2.0.0, then pins the generated baseline to PHP 8.3 and modernises the templates. One item per commit; 67 generator + 13 engine tests throughout; all 6 CI checks green.

PHP 8.3 baseline (breaking)

--min-php and the interactive "Minimum PHP version" prompt are removed. Every scaffold now requires PHP 8.3 — export const MIN_PHP = '8.3', not configurable. validateMinPhp / requiredPhpFor deleted; generated PHPUnit matrix fixed at ['8.3', '8.4']; {{MIN_PHP}} → 8.3 in plugin header, composer.json, readme.txt, ci.yml, phpcs.xml (testVersion 8.3-). The CLI's own PHP CI jobs move to 8.3 so composer install resolves in a generated scaffold.

Templates modernised to that baseline, unconditionally (no {{#if}} forks):

  • Constructor property promotion + readonlyPlugin (container/providers) and the four Woo\Providers\*_Provider service-override constructors lose their property declarations and assignment bodies.
  • Container::get(): mixed is unconditional (supersedes the interim php_8_0 gate / the 7.4 work in the earlier commit).
  • First-class callables — all 38 array( $this, 'method' ) and 3 array( Class::class, 'method' ) hook/command callbacks become $this->method( ... ) / Class::method( ... ) (the ( ... ) spacing is what WPCS's FunctionCallSignature sniff accepts).

match()/enum were considered but have no natural home (no switch, no related-constant clusters) — skipped rather than forced.

Audit findings

#FindingStatus
1generated test:js / lint:js broken on a fresh scaffold✅ jest.config extends wp-scripts' unit config; { virtual: true } mocks; scoped lint; self-consumption CI now runs lint:js/lint:style/test:js
2Interactivity module broken end to endview.js is hand-written ESM at assets/js/view.js, served raw via WP import map
3--min-php 7.4 emitted PHP 8 syntax✅ superseded — PHP version is no longer selectable; baseline is 8.3
4/5free-text escaped by file extension not context (*/, newline, \' in comments)✅ comment/header sanitiser + {{PLUGIN_NAME_ESC}} for PHP strings
6uninstall.php skipped multisite sites past 100get_sites( 'number' => 0 )
7Container re-resolved a null singleton forever (isset)array_key_exists() + new Container_Test.php
8plain-text email escaped entities (Hi O'Brien,)wp_strip_all_tags() alone
9REST get_items_permissions_check() returned bare truecurrent_user_can( 'read' ) · ⚠️ no get_item_schema() — not done
10Gateway::process_payment() marked orders paid✅ fails closed (wc_add_notice + 'failure')
11phpcs.xml didn't scan block render.php./assets/src added with a scoped exclude. */templates/* exclude kept — CI proves the WC email/My-Account templates genuinely fail wp-org WPCS
12vendor in files ships into the zip📄 README release section documents composer install --no-dev before plugin-zip
minorstest script, cache:'npm', stale readme.txt / non-matrix php-version✅ all fixed

Not in this PR

…affold
`npm run test:js` failed 4/4 suites (0 tests run) and `npm run lint:js`
threw ~20 errors on a freshly generated block/interactivity/React plugin.
Verified fixed end to end against a real `npm install`:
- jest.config.js extends @wordpress/scripts' own jest-unit config instead
of setting `preset:` (which dropped the Babel/JSX transform).
- tests/js/*.test.js: `{ virtual: true }` on every jest.mock() of a
@wordpress/* runtime package (they're webpack externals, not on disk);
dropped App.test.js's redundant @jest-environment docblock (jsdom is the
preset default) and view.test.js's dead top-level import.
- Added the @wordpress/* packages the authored JS imports (blocks,
block-editor, i18n, element, interactivity) to devDependencies —
dependency-extraction still externalises them at build, but it clears
import-resolution lint and gives editors types.
- lint:js scoped to `assets/src tests/js` (the wp-scripts-managed source,
not the hand-written jQuery/Elementor enqueue scripts) with
--no-error-on-unmatched-pattern; lint:style scoped + --allow-empty-input
(it errored on scaffolds with no CSS). Dropped the now-redundant
lint-assets CI job.
- prettier fixes in save.js / the two block tests / view.test.js / the
React entry / the generated webpack.config.js string.
- self-consumption CI now runs lint:js + lint:style + test:js on the
generated plugin — the blind spot that let all of this ship in 2.0.0.
Generator suite: 66/66.
…ic bundle
Interactivity.php registered the view as a script module pointing at
assets/build/view.js, but wp-scripts built that entry as a classic IIFE
reading `window.wp.interactivity` — a global core never defines (the
Interactivity API ships only as a script module). The demo threw on click.
wp-scripts can only build a view *module* from a block.json `viewScriptModule`
field, which would force the interactivity module to become a block. But
the view here only imports `@wordpress/interactivity`, a bare specifier
WordPress resolves via its import map — so it needs no bundler at all.
- view.js is now hand-written ESM at assets/js/view.js (alongside main.js /
the widget scripts), and Interactivity.php's wp_register_script_module()
points straight at it.
- Dropped the `view` webpack entry and `hasInteractivity` from the
webpack.config.js-override guard. New `has_webpack_build` flag
(react || block || woo-js) gates the build/start scripts, so
interactivity alone no longer ships a no-op `npm run build`; Jest +
Playwright still ship (needs_build_pipeline).
- view.test.js requires ../../assets/js/view.js; generator tests updated.
Verified: interactivity+block+react scaffold builds, lint:js clean,
test:js 4/4 (view.test.js included). Generator suite: 66/66.
…tainer_Test)
A singleton() factory that resolves to null had its result stored but
isset() reported the slot empty, so the factory re-ran on every get() and
has() returned false for it. Both instance-slot checks in get()/has() now
use array_key_exists().
Adds tests/Unit/Container_Test.php — the composition root's one moving
part had no test at all. Eight cases: bind fresh-per-call, singleton
once, singleton caching a null, instance identity, factory-gets-container,
re-bind clears cache, unknown id throws Not_Found_Exception. Verified the
null-singleton path directly against a generated scaffold. Generator
suite: 66/66.
…author
Free-text values were escaped by file extension, not by where they land:
inside a .php file every {{PLUGIN_NAME}} got `'` -> `\'`, so a plain
docblock became `Composition root for Bob\'s Plugin.` (WordPress renders
the backslash). And a `*/` in --description closed the plugin-header
comment early -> parse error; a newline in --name injected a second
`Plugin Name:` header line.
Now:
- {{PLUGIN_NAME}} / {{DESCRIPTION}} / {{AUTHOR}} in any non-JSON file are
comment-safe: `*/` -> `* /`, newlines collapsed to spaces.
- a new {{PLUGIN_NAME_ESC}} carries the PHP-single-quoted-string escaping
(backslash first, then quote); the 18 `__( '{{PLUGIN_NAME}} …' )` /
sprintf sites now use it.
- JSON keeps its JSON.stringify path (unchanged).
Verified: `--name "Bob's */ Plugin" --description $'…*/…\nRequires PHP: 5.2'
--author "O'Brien */ & Co"` generates a plugin where every PHP file parses,
the header stays one line per field, and composer.json is valid JSON.
Generator suite: 66/66.
…defaults
- #3 validateMinPhp() now rejects < 8.0 (templates use `mixed`, typed
properties, promotion — sub-8.0 generates code that fatals).
- #6 uninstall.php's multisite loop passes get_sites( 'number' => 0 )
so networks past 100 sites are cleaned; comment points at a batched
path for very large networks.
- #8 the plain-text email template used esc_html() — which turns
O'Brien into "O&#039;Brien" in the inbox. Switched to
wp_strip_all_tags() (what WooCommerce core's plain templates do).
- #9 Rest_Controller::get_items_permissions_check() defaults to
current_user_can( 'read' ) instead of a bare `return true;`;
test asserts it gates rather than blanket-allows.
- #10 Gateway::process_payment() fails closed (wc_add_notice + result
'failure') instead of calling payment_complete() on an unconfigured
stub; the success flow is shown in the docblock. New test.
- #11 phpcs.xml now scans ./assets/src (block render.php files) when
there's a webpack build, with a scoped exclude for
PrefixAllGlobals.NonPrefixedVariableFound on */blocks/*/render.php
(those "globals" are WP_Block::render() locals).
Generator suite: 66/66.
…up to
- Generated node-build CI job drops `cache: 'npm'`: setup-node fails the
step when there's no committed package-lock.json (there isn't on a
fresh scaffold).
- The single-version phpcs / integration CI jobs use {{MIN_PHP}} instead
of a hardcoded '8.2', so they never run below the version the plugin
claims to support. (The PHPUnit matrix was already dynamic.)
- Generated package.json gets `"test": "npm run test:js"` when Jest is
present, so `npm test` no longer errors "missing script".
- {{TESTED_UP_TO}} default 6.8 -> 6.9.
Generator suite: 66/66.
CI's wp-org lint (WordPress-Docs) flagged three docblocks added/edited by
the audit fixes:
- Container_Test.php: four test-method short descriptions opened with a
lowercase method name (bind(), singleton(), instance(), get()) ->
Squiz.Commenting.FunctionComment.ShortNotCapital.
- Interactivity.php::register_script_module(): the long description opened
with the lowercase path "assets/js/view.js".
- Gateway_Test.php: the '__' => ... stub arrow was one space short of the
'wc_add_notice' => key, tripping WordPress.Arrays.MultipleStatementAlignment.
Reworded to lead with a capital and realigned the array. No behaviour change.
CI's self-consumption lint (the first run of `npm run lint:js` against a
real scaffold, added for audit #1) surfaced five ESLint/Prettier errors in
three Woo integration files that had never been linted:
- blocks/cart-summary/index.js: useBlockProps() was called from a lowercase
`edit: () => {}` arrow -> react-hooks/rules-of-hooks. Extracted a named
`Edit()` component and hoisted the placeholder string to a module const
("Cart Summary (live on the frontend)" -> "Cart summary" so the __() call
stays on one line for any realistic slug).
- blocks-integration.js: `'{{SLUG}}-order-meta'` inline in createElement
pushed the line past printWidth once the slug was substituted. Hoisted to
`const cls`.
- wc-gateway-block.js: the label fallback `__( '{{PLUGIN_NAME}}', '{{SLUG}}' )`
went over printWidth with real values (and was unescaped). Replaced with a
raw `{{PLUGIN_NAME_ESC}}` const (brand names aren't translated anyway),
dropped the now-unused `__` alias, and pre-broke the `Content` createElement
to the multi-line form Prettier wants.
Verified end to end on a regenerated all-modules+React scaffold:
lint-js / lint-style / build / test-unit-js all exit 0.
The 8.0 floor from audit #3 was heavier than needed: the only 8.0-only
construct in the whole template set is the `: mixed` return type on
Container::get(). Everything else the templates use (arrow functions,
typed properties, null-coalescing assignment) is PHP 7.4.
- validateMinPhp: floor at 7.4 (was 8.0); reject < 7.4.
- requiredPhpFor(modules): new helper, mirrors requiredWpVersion — returns
the highest of 7.4 and any per-module floor (MODULE_PHP_FLOOR, empty
today). Non-interactive uses it as the default and rejects an explicit
--min-php below it; interactive reconciles after module selection since
the PHP prompt runs first.
- templateFlags.php_8_0 (minPhp >= 8.0) gates `: mixed` in Container.php
via an inline {{#if}} — 8.0+ targets keep the hint, 7.4 drops it.
- Generated PHPUnit matrix + phpcs testVersion + composer "php" + plugin
header all already follow {{MIN_PHP}}; CI matrix list gains a 7.4 leg.
Verified: 68/68 unit tests; all-modules 7.4 scaffold — php -l clean, JSON
valid, no `: mixed` / stray tags, CI matrix ['7.4'..'8.3'].
BREAKING: --min-php is gone and the interactive "Minimum PHP version"
prompt (#9) is removed. Every scaffold now requires PHP 8.3 — a single
`export const MIN_PHP = '8.3'`, not configurable. validateMinPhp,
requiredPhpFor and MODULE_PHP_FLOOR are deleted; the generated PHPUnit
matrix is a fixed ['8.3', '8.4']; {{MIN_PHP}} resolves to 8.3 in the
plugin header, composer.json, readme.txt, ci.yml and phpcs.xml
(testVersion 8.3-).
Templates modernised to that baseline (unconditional — no {{#if}} forks):
- Constructor property promotion + readonly: Plugin (container/providers)
and the four Woo\Providers\*_Provider service-override constructors lose
their property declarations and assignment bodies.
- Container::get(): mixed is unconditional again (the php_8_0 gate is gone).
- First-class callable syntax: all 38 `array( $this, 'method' )` and the 3
`array( Class::class, 'method' )` hook/command callbacks become
`$this->method(...)` / `Class::method(...)`.
match()/enum were considered but have no natural home — no switch
statements, no related-constant clusters. Skipped rather than forced.
Verified: 67 generator + 13 engine tests; full-module and minimal 8.3
scaffolds — php -l clean, {{MIN_PHP}} -> 8.3 everywhere, no stray tags.
composer lint / PHPCS on the modern output is exercised by the
self-consumption + verify CI jobs.
…tures
The self-consumption and verify jobs set up PHP 8.2, so `composer install`
in a generated scaffold now fails ("requires php >=8.3 but your php
version (8.2.33)"). Bump both setup-php steps to 8.3.
scripts/verify.js passed `--min-php 8.2` to the elementor fixture — that
flag no longer exists and made the generator exit non-zero. Removed.
wp-scripts/WPCS's FunctionCallSignature sniff treats `$this->m(...)` as a
zero-space function call and flags "Expected 1 space after/before
parenthesis" (phpcbf rewrites it to `( ... )`). PHP accepts whitespace
around the `...` token, so emit `$this->m( ... )` / `Class::m( ... )`
directly. Test assertions updated to match.
@akshat009akshat009 changed the title Fix 2.0.0 audit findings (JS test suite, Interactivity build, Container, escaping, +)Fix 2.0.0 audit findings + pin generated plugins to PHP 8.3Aug 29, 2026
@akshat009
akshat009 merged commit 763f691 into mainAug 29, 2026
6 checks passed
Sign up for freeto 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.

1 participant

@akshat009