Skip to content

R2 — WordPress bridge PHP internals (verified Sept 2026, WP 6.0–7.1)

Verdicts

  1. get_plugins(): requires require_once ABSPATH . 'wp-admin/includes/plugin.php' on WP <= 6.7. Since 6.8, wp-settings.php already includes it (line 527), so the function exists after wp-load. Bridge must use: if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } The include file is definitions-only; safe with WP_USE_THEMES=false. Sources: developer.wordpress.org/reference/functions/get_plugins/ · WP 6.8 raw source (wp-admin/includes/plugin.php, wp-settings.php:52,115,444,527).

  2. get_option('active_plugins'): available everywhere after wp-load (core itself reads it in wp_get_active_and_valid_plugins during bootstrap). Entries are 'folder/file.php' strings.

  3. do_action('litespeed_purge_all'): no-op when LiteSpeed inactive — do_action with zero listeners returns early. LiteSpeed docs state the API hook "works whether or not LSCWP is active". No function_exists guard needed.

  4. wp_upload_dir(): available after wp-load; bridge must call wp_upload_dir( null, false ) — default $create_dir=true attempts mkdir as a side effect. Check the error key.

  5. Translations: since 6.5, runtime loads .l10n.php INSTEAD of .mo when present (both kept as candidates). No core runtime path reads .po. Deleting .po is runtime-safe but NOT durable: wp.org language packs re-ship .po+.mo+.l10n.php, and translations auto-update by default (since 3.7) via the wp_version_check twicedaily cron -> wp_maybe_auto_update -> WP_Automatic_Updater (translation offers carry autoupdate). Manual trigger: update-core.php "Update Translations" button. NOTE: wp_update_translations() does NOT exist in core 5.0–7.x — never reference it.

  6. fileperms pattern: $p = @fileperms( $f ); $perms = ( false === $p ) ? null : substr( sprintf( '%o', $p ), -4 ); — PHP manual Example #1. Check for false first (sprintf('%o', false) === '0' silently).

  7. PHP/WP error-display interplay (critical for the bridge):

  8. hash_equals(known, user) verified timing-safe.
  9. php://input + json_decode(..., JSON_THROW_ON_ERROR) verified; JsonException is Throwable-catchable.
  10. catch (Throwable) covers TypeError and engine Errors but NOT hard fatals (OOM, max_execution_time, failed require, parse errors).
  11. During wp-load, wp_debug_mode() (wp-settings.php:84) sets display_errors=0 for JSON requests (detected via Content-Type/Accept headers!) and rewrites error_reporting when WP_DEBUG=false. Therefore the bridge MUST set error_reporting(E_ALL); ini_set('display_errors','1'); AFTER wp-load returns.
  12. Core registers NO set_error_handler during bootstrap; restore_error_handler() after wp-load is harmless belt-and-braces vs plugins.
  13. WP registers a fatal-error SHUTDOWN handler that masks hard fatals with the generic "critical error" page (the opaque-500 failure mode). Bridge defines WP_DISABLE_FATAL_ERROR_HANDLER BEFORE wp-load so hard fatals surface raw; the client detects non-JSON responses and reports "hard fatal + body excerpt".
  14. json_decode flags: use JSON_THROW_ON_ERROR.

Bridge recipe (binding for W4)

<?php
define( 'WP_USE_THEMES', false );
define( 'WP_DISABLE_FATAL_ERROR_HANDLER', true );   // before wp-load
require_once __DIR__ . '/wp-load.php';
error_reporting( E_ALL );                            // AFTER wp-load (survives)
ini_set( 'display_errors', '1' );
if ( ! function_exists( 'get_plugins' ) ) {
    require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
// ... token gate, action dispatch, try/catch (Throwable) -> JSON