Drop the eslint job, which could never have passed here
This plugin ships no JavaScript: no js/ directory, no scripts, no
package.json. The workflow carried a "JS coding standards and tests" job
anyway, and it did not fail at the linter -- it never reached one. It died
at actions/setup-node, looking for a lock file:
Dependencies lock file is not found in /home/runner/work/...
Supported file patterns: package-lock.json, npm-shrinkwrap.json, yarn.lock
A job that cannot pass is worse than no job. It puts a red mark on every
push for a reason nobody can act on, and teaches everyone to stop reading
the marks.
The shared standard said "a plugin with no js/ directory deletes the whole
eslint job". This plugin has no js/ directory, so the job should have gone
when the workflow was written; the check that compares a workflow to the
template was tolerating its absence rather than requiring it, so nothing
noticed either way.
Recent Commits to wp-pluginsused:master
-
Drop the eslint job, which could never have passed here
-
Name the screen, not the plugin, in the page heading
Name the screen, not the plugin, in the page heading "WP-PluginsUsed" as the h1 duplicated the sidebar entry. The heading says what the screen is now -- Plugins Used -- matching Ban Options, Manage Ratings, Stats and the rest. STANDARDS.md 4.1: the prefix belongs in the menu title, never in the heading. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018W7cKqYNL1gUFTi38VRHaX
-
Correct the record on wp_get_sites(), here and in the CI comment
Correct the record on wp_get_sites(), here and in the CI comment This plugin repeated a claim that runs through the whole collection: that wp_get_sites() was removed in WordPress 5.1 and that calling it fatals. Neither is true. It was deprecated in 4.6 and still ships in ms-deprecated.php, which is loaded for multisite only -- so on a single site it merely looks absent. What it actually does is worse than a crash: it returns only the first 100 sites, so network activation and uninstall quietly did their work on a hundred sites and reported success. Comments, docblocks, assertion messages and the test method names that encoded the false version are all corrected to say that. No behaviour changes -- the code already used get_sites() with 'number' => 0. The CI matrix comment carried the same claim and moves with the shared template. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018W7cKqYNL1gUFTi38VRHaX
-
Stop claiming what the reader is upgrading from
Stop claiming what the reader is upgrading from The Upgrade Notice said the floors moved 'up from 6.0 and 7.4', or advised anyone 'still on PHP 7.4' to move. Both are wrong about the version actually on wordpress.org. Queried against plugins.svn.wordpress.org, the released readmes declare WordPress 2.8 to 5.5, and fifteen of the nineteen declare no Requires PHP at all. Three plugins had ended up asserting three different predecessors, none of them right - and telling a reader who may be on 5.6 that they are on 7.4 is worse than saying nothing, because it reads as confirmation they are fine. What the notice can state truthfully is the requirement, the consequence and how to check: an older site is not offered the update, and Site Health reports the PHP version. That holds wherever the reader is coming from.
-
Emit the markup kses leaves, and assert loss rather than bytes
Emit the markup kses leaves, and assert loss rather than bytes The plugin has two ways out: the shortcodes return their markup, while display_pluginsused() echoes it through wp_kses(). kses normalises as well as filters, so the two paths were emitting different bytes for the same listing - viewBox lowercased to viewbox, a space added before a self-closing slash, ' rewritten to ' inside a title. A theme calling the template tag got markup the shortcode never produced. The icon now spells its attributes the way kses leaves them, so the two paths agree. Lowercase viewbox is safe and is what the echoing path has always shipped: an HTML parser applies its SVG case-fixup table, so it reaches the DOM as viewBox and the icon still scales. The entity rewriting cannot be pre-empted that way, so the two tests now assert what they mean rather than byte-identity. The allow-list test compares inventories of tags and attribute names, since an incomplete allow list drops a tag or an attribute and that is the failure worth catching. The template-tag test compares the echoed output against wp_kses( render(), allowed_html() ), which is what it should equal. Also made the settings_fields() assertion quote-agnostic: core quotes option_page with single quotes, which is not part of any contract. 158 tests, 442 assertions, green.
-
Match wp_enqueue_script( with its bracket, not as a substring
Match wp_enqueue_script( with its bracket, not as a substring The no-jQuery test forbade the bare string 'wp_enqueue_script', which is also a substring of the ACTION name wp_enqueue_scripts - the hook a plugin legitimately uses to enqueue a stylesheet. Registering no scripts is not the same as never touching the hook scripts are registered on. wp-stats failed on exactly this: it hooks wp_enqueue_scripts to add its one sheet. The others here pass today only because they happen not to, so this is a latent false failure waiting for the first stylesheet. Found by the first PHPUnit run of this work.
-
Re-lock composer against the PHP 8.2 floor
Re-lock composer against the PHP 8.2 floor composer.json moved to >=8.2 with the floor raise but the lock file still recorded >=7.4. Composer treats that as an error rather than a warning, so 'composer install' would have failed inside the tests container before PHPUnit ran a single test - and CI does exactly that on all six matrix rows. bin/verify.py now compares the two, so the pair cannot drift again.
-
Step 12: drop the last two phpcs suppressions and fix the code under …
Step 12: drop the last two phpcs suppressions and fix the code under them Both were in includes/, both had a reason written next to them, and the target for this collection is zero. display_pluginsused() echoed its markup behind an EscapeOutput ignore. It now escapes at the sink like everything else, through wp_kses() with an allow-list that says exactly which tags and attributes the listings use. A test asserts wp_kses() leaves all three listings byte-identical, so the day the markup grows a tag the list has not got, the suite says so rather than the template tag quietly printing less than the shortcodes do. The summary sentence carried an I18n.NoHtmlWrappedStrings ignore arguing that rewording the msgid would orphan its translations. That is true and it is still the right trade: a translator has no business being handed <strong>, and a msgid wrapped in a tag is a translation waiting to lose it. The tags moved into the concatenation, the rendered string is byte-for-byte what it was, and the three orphaned msgids are recorded as a NOTE in the changelog. phpcs and php -l are clean across the plugin, and bin/verify.py reports zero.
-
Step 11: put the suite on the house conventions and add the shared tests
Step 11: put the suite on the house conventions and add the shared tests Files and classes follow 7.1: helper-fixtures.php becomes helper-testcase.php because what it declares is the base test case, test-stats.php becomes test-summary.php so nothing confuses it with the WP-Stats integration this plugin does not have, and every class is WP_PluginsUsed_<Area>_Test extending WP_PluginsUsed_TestCase. test-bootstrap.php and test-uninstall.php extended WP_UnitTestCase directly and now go through the shared base like the rest. Discovery is by the test- prefix now, so a misnamed file would be silently skipped rather than fail: 109 test methods before this step, 150 after, none lost in the rename. tests/test-metadata.php carries the fourteen shared assertions from 7.2, plus three specific to what this plugin just did: that every fired hook carries the prefix, that no apply_filters_deprecated shim crept back in, and that the Upgrade Notice names all three renamed filters in both spellings. The last one is the guard that matters -- the rename is only survivable for a site owner if the notice actually lists it. run_uninstall() is shared rather than repeated, because uninstall.php declares a function and so can only be included once per process. Two files assert against it; the second include would have been a silent no-op and the test would have passed on rows nothing had deleted.
-
Step 2 (redo): re-sync the shared config files after the templates moved
Step 2 (redo): re-sync the shared config files after the templates moved _standards/templates/phpcs.xml and phpunit-multisite.xml.dist changed while this plugin was being worked through, so the copies taken at 516e33a had drifted from the shared originals. Both are re-copied verbatim. Neither new exclusion applies here -- WP-PluginsUsed owns no database table and has no list table -- but the point of these files is that all nineteen carry the same one, so it is copied whole rather than trimmed to fit.
-
Step 10: put the readme on the canonical section set
Step 10: put the readme on the canonical section set The six h2s in the order the standard fixes them: Description, Usage, Frequently Asked Questions, Screenshots, Changelog, Upgrade Notice. Usage was an h3 buried in Description and the changelog sat above the screenshots. The License line gets its two trailing spaces back. Without them Markdown ran "License: GPLv2 or later" and the License URI together into one line on GitHub, which is invisible in the source and in a diff. Description gains a Features list and keeps Donations as its last h3, in the one wording all nineteen use, without the stray bullet it carried. Development and Credits are gone: the repo link belongs in the repo, and the Flaticon credit was for the wordpress.org listing icon rather than anything this plugin ships -- worth re-adding to the .org assets if it is still owed. Changelog entries are sorted into BREAKING, NEW, CHANGED, FIXED, NOTE within 2.0.0. The security fix moves from a sixth prefix, SECURITY:, to FIXED:, the raised floors become their own BREAKING line, and four 2008-era bullets that were indented with two spaces are normalised so they parse as bullets at all.
-
Step 9: scope the emitted class names and drop the last inline style
Step 9: scope the emitted class names and drop the last inline style The plugin ships no stylesheet, but it does emit markup, and the two class names on the state icons were unscoped. They become wp-pluginsused-icon, wp-pluginsused-icon-active and wp-pluginsused-icon-inactive, so a theme has one prefix to target and no chance of colliding with anything else. The SVGs lose their style="vertical-align: middle" as well. It was the last inline CSS in the plugin, and the icons still inherit their colour through currentColor, which is the part that actually matters for a theme. There is no css/ or js/ directory to normalise -- the plugin has neither, and the SVG is built in PHP -- so this is the whole of steps 7 to 9. Steps 7 and 8 are no-ops: no JavaScript, and the GIF markers were already replaced by inline SVG earlier in this major.
-
Step 6: put the settings screen on the house rules
Step 6: put the settings screen on the house rules The screen was already Settings API rather than hand-written markup, so this is the naming and the details around it: * CAPABILITY, SECTION_DISPLAY and SECTION_HIDDEN join PAGE and GROUP, and the two section ids and both field ids gain the plugin's prefix. * Every capability check goes through capability(), which fires wp_pluginsused_capability with a context, so a site hands the screen to a different role in one place rather than by hunting current_user_can() calls. * register() and render() become register_settings() and render_page(), matching the other settings screens in the collection. * The hidden-plugins list loses its inline style attribute and separates its labels with <br />, the way core's own checkbox lists do. That was the only inline CSS in the plugin. Five tests follow, including two standing guards: no inline style attribute reaches the screen, and no file hand-writes a form table.
-
Step 5: prefix the three public filters and write the Upgrade Notice
Step 5: prefix the three public filters and write the Upgrade Notice pluginsused_show_version -> wp_pluginsused_show_version pluginsused_hidden_plugins -> wp_pluginsused_hidden_plugins pluginsused_plugins_used -> wp_pluginsused_plugins_used All three shipped in 1.50, so this genuinely breaks third-party code. The old names are dropped outright -- no apply_filters_deprecated() shims, which was the decision taken for the whole collection: a shim keeping an unprefixed name alive keeps the clash the rename exists to remove. Two more unprefixed identifiers go with them, for the same reason and in the same release rather than one at a time: the PLUGINSUSED_SHOW_VERSION constant and the $pluginsused_hidden_plugins global, which were how 1.50 was configured by editing the plugin's own file. The settings screen added in this major does both jobs and survives updates, which is what those two never did. The Upgrade Notice is written for a site owner: what silently stops working, where in their site to look for it, what the symptom will be weeks later, and the reassurance that no code of their own means nothing to do. The changelog carries the same two BREAKING lines in developer shorthand. Tests follow: the filter call sites are renamed, and the two that asserted the legacy global and constant still worked now assert that they do not. -
Step 4: give the plugin the two canonical option rows
Step 4: give the plugin the two canonical option rows Settings move from pluginsused_options to wp_pluginsused_options, and the version markers get a row of their own, wp_pluginsused_version, holding 'plugin' and 'db' and nothing else. Keeping them apart means the settings screen and the upgrade routine can never overwrite each other, and the sanitise callback never has to rescue a value the form did not post. There was no migration here to retarget -- 1.50, the last release, stored nothing at all, so the unprefixed row only exists on an install that ran a development build of 2.0.0. maybe_upgrade() is that one migration: it folds the old row in, re-sanitises it, deletes it, then stamps both markers in a single write so a half-finished upgrade cannot record itself as complete. It runs on activation and on every admin load, because activation hooks do not fire on update, which is the usual reason a migration never runs. uninstall.php removes all three rows -- both current ones and the unprefixed leftover, for an install removed before it ever reached wp-admin. Seven new tests cover the fold, the re-sanitise, the refusal to overwrite a newer prefixed row, idempotency, and the shape of the marker row.
-
Step 3: prefix the classes, add the six constants, raise the floors
Step 3: prefix the classes, add the six constants, raise the floors PluginsUsed, PluginsUsed_Options, PluginsUsed_Settings and PluginsUsed_Template become WP_PluginsUsed_*, each in the class-wp-pluginsused-* file its name now implies. Nothing unprefixed is left in the global namespace except the template tag and the deprecated shims, which are the documented public API and keep the names they shipped with. The plugin file gains the four constants it was missing -- DB_VERSION, SLUG, DIR and URL -- and loads includes/ through DIR rather than __DIR__. Floors go to WordPress 6.8 and PHP 8.2 in the plugin header and the readme; phpcs.xml, .wp-env.json, composer.json and the CI matrix already carried them from the templates. There is no version-guarded back-compat shim to delete: the plugin has never branched on a PHP or WordPress version. The one test that asserted the classes must NOT be prefixed with WP_ now asserts the opposite, and that each one lives in the file its name implies.
-
Step 2: adopt the shared config files from _standards/templates/
Step 2: adopt the shared config files from _standards/templates/ .editorconfig, .gitignore, composer.json, phpcs.xml, both phpunit configs, bin/test.sh, bin/test-multisite.sh, every index.php and ci.yml now come from the templates verbatim, with only the placeholders substituted. The eslint job goes with them: this plugin ships no JavaScript, so there is nothing to lint and no package.json, eslint.config.mjs or vitest.config.mjs to add. .wp-env.json gains the ports assigned in STANDARDS.md 10, 8906 and 8907, so this plugin's environment can no longer collide with a sibling's. The template's /tests/* exclusion list is closed, which cost four fixes here rather than four new exclusions: * The fixtures write their plugin files through WP_Filesystem instead of mkdir()/file_put_contents()/unlink()/rmdir(), with the transport pinned to "direct" in the bootstrap so it never negotiates credentials. The two tests that built a fixture inline now share the same pair of helpers. * The DOM inspection reads DOMAttr's lowercase name/value and describes the node with getNodePath(), rather than reaching for camelCase DOMNode properties WPCS cannot let through. * Five docblocks opening on a lowercase function name were reworded.
-
Move template-tags.php and deprecated.php under includes/
Move template-tags.php and deprecated.php under includes/ They were the only loose PHP left in the plugin root, which is inconsistent with the rest of the collection: wp-useronline keeps both under includes/ (638001f), and wp-pagenavi and wp-polls do the same with template-tags.php. Only the three files WordPress requires to be there stay at the root -- the main plugin file, uninstall.php, and the index.php silence guard. Adds a test pinning that layout, so the next file cannot quietly land in the root again. Verified after the move: 115 tests single-site and multisite, the Playground harness 84/84 on PHP 7.4 / WP 6.0 and PHP 8.3 / WP latest, and a deploy dry-run confirming the shipped tree is the same 12 files in their new places. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TFsMxKw9RRmE5Kk41hAxYo
-
Cover multisite in CI and bin/test.sh
Cover multisite in CI and bin/test.sh The suite passed as multisite already; nothing was pinning it that way, so nothing would have caught a regression. Two behaviours only exist on a network: uninstall.php branches on is_multisite(), and network-activated plugins are read from active_sitewide_plugins, a site option the single-site run never touches. Running as a network also makes the uninstall test execute the get_sites() loop and the switch_to_blog()/restore_current_blog() pair for real rather than only asserting on the source. WP_MULTISITE is read by the WordPress test bootstrap inside the container, so it is forwarded in rather than exported around the npx process. 114 tests / 220 assertions green single-site and multisite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TFsMxKw9RRmE5Kk41hAxYo
-
Run CI on Node 24
Run CI on Node 24 Node 20 went end-of-life in April 2026. This standardises every plugin on 24, the current active LTS (Krypton), which wp-polls was already using. This is the Node that runs npx @wordpress/env for the test suite, not the Actions runtime -- that was the separate checkout/setup-node v7 bump -- so it had to move on its own.
-
Bump actions/checkout and actions/setup-node to v7
Bump actions/checkout and actions/setup-node to v7 GitHub is deprecating Node 20 on Actions runners, so checkout@v4 and setup-node@v4 were being force-run on Node 24 and emitting a warning on every job. v5 moved these actions to Node 24; v7 is the current major. Neither breaking change affects this workflow: setup-node v6 limited automatic caching to npm, and no cache input is passed here; checkout v7 blocks checking out fork PRs for pull_request_target and workflow_run, and this triggers on push and pull_request. shivammathur/setup-php stays at v2 -- that is still its current major and it was not in the deprecation warning.
-
Expand the test suite from 67 to 114 tests
Expand the test suite from 67 to 114 tests New coverage: - test-bootstrap.php: constants agree with the plugin header and the readme Stable tag, every code file carries its ABSPATH guard, the classes avoid the WP_ prefix core reserves, and the admin hooks are absent on the front end. - test-stats.php: the summary sentence pluralises through three independent _n() calls, so singular, plural and zero are three code paths. None were covered; the singular form had never been rendered by a test. - Uninstall is now executed, not just read: it deletes the option for real on the single-site branch. Added checks that it references no plugin class or function (it runs with the plugin inactive) and that the plugin writes no option row other than the one uninstall names. - Template: hiding matches whole names rather than substrings, no trailing space when the version is suppressed, absent URIs emit no anchor, descriptions are still texturized, the per-request cache holds and reset_cache() clears it, icons carry role/aria-label and differ by shape, and nothing enqueues a script or stylesheet. - Settings: a plugin name containing quotes round-trips through the form and still matches what it hides, unknown keys are dropped, ticked boxes render as checked. - Deprecated: the render path must never call a shim, or every page load would emit deprecation notices under WP_DEBUG. Two harness bugs the new tests exposed, both fixed: - GLOB_BRACE is a GNU extension and is undefined in the container's PHP, so three tests errored. Source files are now collected with two glob() calls. - Searching raw source matched comments, not code: the docblock in class-pluginsused-template.php explaining that pluginsused_format_display() forwards to it counted as a call site. Source inspection now goes through php_strip_whitespace(), the same fix the uninstall test already needed. phpcs.xml needed no new exemptions. helper-source.php is split out of helper-fixtures.php because a file may declare either functions or a class. 114 tests / 220 assertions green on PHP 7.4 / WP 6.0 and PHP 8.3 / WP latest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TFsMxKw9RRmE5Kk41hAxYo
-
Add PHPUnit suite, CI, and update the readme for 2.0.0
Add PHPUnit suite, CI, and update the readme for 2.0.0 67 tests / 122 assertions against a real WordPress database via wp-env, run on both ends of the support matrix (PHP 7.4 / WP 6.0 and PHP 8.3 / WP latest). Nothing here ships: plugin_deploy.sh already excludes tests/, bin/, phpunit.xml*, composer.*, .wp-env.json and .git*. The tests drive the plugin's own entry points against fixture plugins written to disk, because every defect worth catching here is integration, not logic: - test-escaping.php parses the output with DOMDocument and asserts no event-handler attribute and no javascript: URL survives into the DOM, while the hostile name is still displayed as text. Substring matching would be wrong: once escaped, "onmouseover="" still occurs inside an attribute value, where it is inert. - test-uninstall.php asserts on php_strip_whitespace() output rather than the raw file. Matching the raw source passes on the comment that documents 'number' => 0 and keeps passing after the argument itself is deleted -- confirmed by deleting it. - test-template.php covers the multisite active_sitewide_plugins fix, ordering, and that hidden plugins leave the counts as well as the listings. Every assertion was verified to fail against deliberately broken code. phpcs.xml relaxes docblock, file-naming and DOM-property rules under tests/ only, so CI can fail on warnings everywhere else. Adds .gitignore: wp-env installs dev dependencies into the mounted plugin directory, so vendor/ appears on the host and must not be committed. Readme: documents the settings screen, the three filters and the template tag, trims Tags to the five-tag maximum, drops the dead dev.wp-plugins.org translations link and the FamFamFam credit for the removed GIFs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TFsMxKw9RRmE5Kk41hAxYo
-
Restructure into includes/, add a settings screen, drop the GIF icons
Restructure into includes/, add a settings screen, drop the GIF icons Replaces the hand-rolled plugin scanner and the edit-the-source config with current WordPress APIs. Behaviour is unchanged: the three shortcodes, the display_pluginsused() template tag and the rendered markup all keep working. - Scanning: get_plugins() replaces the bespoke directory walk and the "|Plugin Name:(.*)|i" header regexes, which read whole files and could match a "Version:" occurring anywhere in one. Core sorts identically (_sort_uname_callback is the same strnatcasecmp on name), so ordering is byte-for-byte what it was. - Config: settings now live in one option row, pluginsused_options, behind Settings -> WP-PluginsUsed. Previously the only way to hide a plugin or a version number was to edit wp-pluginsused.php, which every plugin update silently reverted. The PLUGINSUSED_SHOW_VERSION constant and the $pluginsused_hidden_plugins global are still honoured, and two new filters (pluginsused_show_version, pluginsused_hidden_plugins) are applied last. - Icons: inline SVG replaces images/plugin_{active,inactive}.gif. They stay crisp at any pixel density, inherit the theme's text colour, cost no HTTP request, and distinguish state by shape as well as by an aria-label. - Multisite: network-activated plugins were listed as inactive, because only active_plugins was consulted. active_sitewide_plugins is now merged in. - Empty Plugin URI / Author URI no longer emit <a href="">. - Drops load_plugin_textdomain(); WordPress has loaded plugin translations automatically since 4.6, and since 6.7 an early call triggers _doing_it_wrong. - Old procedural functions remain in deprecated.php as working shims. Layout follows the Plugin Handbook: includes/class-*.php, index.php silence guards, an uninstall.php whose multisite loop passes 'number' => 0 and restores inside the loop. Verified with a WordPress Playground harness, 84 assertions, on both PHP 7.4 / WP 6.0 and PHP 8.3 / WP latest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TFsMxKw9RRmE5Kk41hAxYo -
Apply WordPress Coding Standards (phpcbf)
Apply WordPress Coding Standards (phpcbf) Formatting only, no logic change: 96 violations fixed automatically. Adds phpcs.xml so the ruleset is pinned to PHP files (WPCS has had no JavaScript sniffs since 3.0) and CI uses the same config as local runs. Verified against the Playground harness: 33/33 assertions unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TFsMxKw9RRmE5Kk41hAxYo
-
Fix stored XSS in plugin listing output
Fix stored XSS in plugin listing output pluginsused_format_display() interpolated plugin header values straight into href/src/alt/title attributes. strip_tags() was the only filtering applied, and it removes tags but leaves quotes, so a plugin whose header contains a double quote broke out of the attribute: Plugin Name: Evil" onmouseover="alert(1) Plugin URI: javascript:alert(2) rendered as a live onmouseover handler on both the <img> and the <a>, plus href="javascript:alert(2)". Escape at each sink with esc_url(), esc_attr() and esc_html() instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TFsMxKw9RRmE5Kk41hAxYo