Skip to content

fix: skip ghost plugin entries and drop descriptions in SITE.md Active Plugins - #3523

Merged
chubes4 merged 1 commit into
mainfrom
fix/3521-site-md-ghost-plugins
Sep 19, 2026
Merged

chubes4 merged 1 commit into
mainfrom
fix/3521-site-md-ghost-plugins

Conversation

@chubes4

@chubes4 chubes4 commented Sep 19, 2026

Copy link
Copy Markdown
Member

Fixes #3521

The bug (ghost entries)

inc/setup/site-md.php:322 (datamachine_site_section_plugins()) guarded the description lookup with file_exists() but printed the plugin name unconditionally from the active_plugins option:

if ( function_exists( 'get_plugin_data' ) && file_exists( $plugin_path ) ) {
    $plugin_data = get_plugin_data( $plugin_path, false, false );
    $plugin_desc = ! empty( $plugin_data['Description'] ) ? $plugin_data['Description'] : '';
}

Live proof on extrachill.com:

$ wp plugin is-installed breeze     # exit 1
$ ls wp-content/plugins/breeze      # No such file or directory

Yet SITE.md line 56 listed - **breeze** — the only entry in the entire Active Plugins list with no description, which is exactly what this code path produces for a deleted-but-not-deactivated plugin: it fell through to the else branch's bare-name fallback since file_exists() failed, but get_plugin_data() never having been asked for a name meant the raw dirname()/basename() fallback ('breeze') got printed with $plugin_desc = ''. Breeze was replaced by extrachill-cache months ago and deleted from disk without ever being deactivated — the active_plugins option still carried the stale entry, and SITE.md, being always-on AI context, was maximally privileged in telling agents a caching plugin was active that didn't exist.

Same defect, same file

Grepped every get_plugin_data / file_exists pair in the file. datamachine_network_section_plugins() at (pre-fix) line 583 had the identical pattern for the NETWORK.md "Network Plugins" block (network-activated plugins) — fixed identically.

datamachine_site_section_mu_plugins() and datamachine_site_section_dropins() use core's get_mu_plugins() / get_dropins(), which enumerate the actual wp-content/mu-plugins and wp-content directories via opendir()/readdir() rather than reading a stored option — verified against wp-admin/includes/plugin.php. They cannot produce ghost entries structurally, so no fix was needed there.

Fix

Both datamachine_site_section_plugins() and datamachine_network_section_plugins() now continue past the entry entirely when file_exists( $plugin_path ) is false, before ever computing a name. function_exists( 'get_plugin_data' ) is now checked independently, so a plugin that genuinely exists on disk still gets a real name even in the rare case get_plugin_data() isn't loaded yet.

The scope reduction (drop descriptions)

The ## Active Plugins block was 6,076 of SITE.md's 8,726 total bytes (issue reported ~6,097/8,726 — consistent, small option-list drift since). SITE.md is injected into every AI call as always-on context; the per-plugin Description header (marketing copy — Gutenberg's is literally "Printing since 1440.") carries near-zero value there, and wp plugin list is one command away for anyone who needs it.

datamachine_site_section_plugins() now emits - **Plugin Name** only, dropping the wp_strip_all_tags( $plugin_data['Description'] ) suffix entirely. Kept the ## Active Plugins heading and list structure unchanged. datamachine_network_section_plugins() (NETWORK.md) never printed descriptions to begin with, so it's untouched on this axis.

Before / after (measured against extrachill.com's live composed SITE.md)

bytes
Active Plugins block, before (descriptions + breeze ghost) 6,076
Active Plugins block, after (names only, ghost dropped) 956
Reduction 5,120 bytes (~84%)

(Computed by extracting the live ## Active Plugins block and simulating both fixes against it — descriptions stripped and the breeze line removed.)

Verification

  • php -l inc/setup/site-md.php — clean.
  • No existing PHPUnit/smoke coverage of site-md.php generation existed. Added tests/site-md-plugin-ghost-entries-smoke.php (standalone smoke, run with php tests/site-md-plugin-ghost-entries-smoke.php, following the repo's existing *-smoke.php convention — no bundled WordPress test suite exists locally). It stubs get_option/get_site_option/get_plugin_data/is_multisite against a real temp WP_PLUGIN_DIR containing one plugin that exists on disk and one that's listed in the options but has no file, reproducing the exact breeze scenario for both SITE.md's Active Plugins and NETWORK.md's Network Plugins. 8/8 assertions pass:
    PASS: plugin present on disk is listed by name
    PASS: plugin missing from disk (deleted without deactivation) is not listed — #3521
    PASS: Data Machine's own entry is still suppressed
    PASS: description text is no longer emitted for Active Plugins (byte-size scope reduction)
    PASS: no description separator appears anywhere in the Active Plugins block
    PASS: network-active plugin present on disk is listed by name
    PASS: network-active plugin missing from disk is not listed — shares the #3521 defect, fixed identically
    PASS: Data Machine's own network entry is still suppressed
    
  • composer lint (phpcs) fails identically on unmodified main — there's no phpcs.xml/phpcs.xml.dist in the repo, so the default PSR2 ruleset (4-space indent) flags every line of this tabs-indented, WPCS-style file. Confirmed via git stash that this is pre-existing and unrelated to this change; the repo's real lint gate runs elsewhere (homeboy baseline config), not via bare composer lint.
  • Ran the full tests/*-smoke.php suite standalone before and after this change (42 pre-existing failures on unmodified main from environment/ordering issues unrelated to site-md.php, e.g. missing DB/WP runtime — identical failure set with this diff applied, confirming no regression).

…e Plugins

Plugins recorded in the active_plugins / active_sitewide_plugins options
but deleted from disk without being deactivated first were still
rendered by name in SITE.md and NETWORK.md, with no way to tell them
apart from a real plugin except a missing description.

- inc/setup/site-md.php:322 (datamachine_site_section_plugins) and
  :583 (datamachine_network_section_plugins) shared the same defect:
  file_exists() guarded the description lookup but not the unconditional
  name print. Both now skip the entry entirely when the plugin file is
  missing from disk.
- get_mu_plugins() / get_dropins() enumerate the filesystem directly, so
  the Must-Use Plugins and Drop-ins sections cannot produce ghost entries
  and were left unchanged.
- datamachine_site_section_plugins() now emits plugin names only, no
  Description header text. SITE.md is always-on AI context and
  descriptions carried near-zero value there; wp plugin list is one
  command away for the full metadata.

Fixes #3521
@chubes4
chubes4 merged commit abb1735 into main Sep 19, 2026
30 checks passed
@chubes4
chubes4 deleted the fix/3521-site-md-ghost-plugins branch September 19, 2026 22:02
Sign up for free to 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.

SITE.md lists deleted plugins as active (ghost entries from the active_plugins option)

1 participant