Make WordPress Core

Opened 4 weeks ago

Last modified 2 weeks ago

#66057 new defect (bug)

get_plugins() cache ignores extra_plugin_headers registered after the first scan

Reported by: Cybr Owned by:
Priority: normal Milestone: 7.2
Component: Plugins Version:
Severity: normal Keywords: has-patch has-unit-tests
Cc: Focuses: tests, performance

Description

get_plugins() stores plugin headers in the plugins object cache on the first scan. get_file_data() applies extra_plugin_headers only while that scan reads files. A later get_plugins() hits the cache and never sees headers registered in between.

That first scan often happens during plugin include, before plugins_loaded. Any plugin that calls get_plugins() there freezes a snapshot with no extra headers. Plugins that register extra headers and then read them get empty values, notices on missing keys, or skip their own update logic.

Reproduction

  1. Plugin A, slug sorting before plugin B, calls get_plugins() in the main file (include time).
  2. Plugin B adds a header on extra_plugin_headers and later calls get_plugins().
  3. Plugin B's extra header key is missing from the cached rows.

get_plugins( '/some-slug' ) is a separate cache slot today, so a folder scan does not reuse a warm all-plugins list and can freeze the same stale headers on its own key.

Expected

Extra headers registered after the first scan should appear on the next get_plugins() call. Already-cached fields (Name, Version, ...) should stay. A folder argument should scan only that folder. SHORTINIT should not prime this cache; only get_plugins() should fill it.

Seen in the wild

​Troy Client #24. Troy Client registers custom plugin headers and uses them to talk to Troy Server and to keep Troy plugins off wordpress.org's update-check. An early get_plugins() meant those headers never landed, so Client could not see its own plugins. Client 1.8.1184 flushes the cache as a workaround. Core should not require that.

WooCommerce has hit the same cache for years. The Helper needs a Woo: extra header to see paid extensions (subscriptions and Woo.com updates). WC tested up to / WC requires at least also vanish, so extensions look untested. Woo already flushes wp_cache group plugins when those keys are missing (WooCommerce PRs: ​#26358, ​#21669). That is a product workaround for this core bug. They knew about the bug, so I'm surprised that they didn't raise an issue with WordPress.

Plugin authors should not have to detect missing keys and flush the plugins cache themselves. A core fix does that work once. Sites that update WordPress then see extra headers on the next get_plugins() call, so custom update-server connections this cache bug severed start working again without a plugin update.

Themes cannot cause this. They load after plugins_loaded.

Change History (4)

This ticket was mentioned in ​PR #13413 on ​WordPress/wordpress-develop by ​@Cybr.


4 weeks ago
#1

  • Keywords has-patch has-unit-tests added

get_plugins() cached the first file scan for the rest of the request. extra_plugin_headers only runs inside get_file_data(), so headers registered after that scan never appeared.

This patch keeps the existing cache shape ($cache[''] is still the all-plugins list; $cache['/slug'] is still that folder):

  • get_plugins( $folder ) uses $cache[ $folder ] when that key exists, including an empty list for a missing folder.
  • A folder call scans only that folder. If '' is already warm, a later folder call slices from it. A cache hit does not walk the plugins directory again.
  • Extra header names are stored in the extra_plugin_headers cache. When the filter set changes, missing keys are merged into the list this call returns. Name, Version, and other cached fields are left alone.

Plugin authors should not have to flush the plugins cache to see late extra headers. Sites that update WordPress get those headers on the next get_plugins() call.

## Test plan

  • [ ] npm run test:php -- --filter test_get_plugins
  • [ ] Warm get_plugins(), then add extra_plugin_headers, then get_plugins() again: new keys present, old fields unchanged
  • [ ] get_plugins( '/some-plugin' ) first: $cache[''] is absent; hello.php is not scanned until get_plugins()
  • [ ] $cache[''] is the plugin list keyed by file (not a nested folder bag)

Trac ticket: https://core.trac.wordpress.org/ticket/66057

## Use of AI Tools

AI assistance: Yes
Tool(s): Cursor
Model(s): Grok 4.6 extra high
Used for: Drafting get_plugins() / _wp_merge_extra_plugin_headers(), the PHPUnit tests, and this description. I directed the cache shape and merge-only-the-returned-list behavior, edited the patch, and verified on a live site (include-time get_plugins() then extra_plugin_headers: FAIL before, PASS after).

#2 @Cybr
4 weeks ago

For themes: We may also want to look into WP_Theme / extra_theme_headers (same header freeze, different cache key), but that should not distract from this ticket.

It does not appear to happen in the wild the same way. Core's first wp_get_theme() is after plugins_loaded, so plugin extras usually land. None of the top 3000 WordPress.org plugins both register those extras and construct WP_Theme at include. A companion calling wp_get_theme() early still makes a miss plausible.

#3 @SergeyBiryukov
4 weeks ago

  • Milestone Awaiting Review → 7.2

This ticket was mentioned in ​Slack in #core-performance by mukeshpanchal27. ​View the logs.


2 weeks ago

Note: See TracTickets for help on using tickets.