Created
July 19, 2026 01:10
-
-
Save patrickfreitasdev/14ac6a0f1433d1246da9ff619b3d1f62 to your computer and use it in GitHub Desktop.
[Defender Pro] Cron and CLI debug
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Plugin Name: [Defender Pro] WP-CLI / deactivation debug (Rev.1) | |
| * Description: Captures WP-CLI runtime details and full backtraces when Defender is deactivated. Drop into wp-content/mu-plugins/. Adapted from https://gist.github.com/wpmudev-sls/3600f4c2f5e1c93d4b4afa81b0423f4c | |
| * Author: WPMU DEV Support | |
| * License: GPLv2 or later | |
| * | |
| * IMPORTANT: Must live in mu-plugins/ so it loads before Defender. If deactivate_plugins() | |
| * runs during wp-defender.php include, a normal plugins_loaded hook is too late. | |
| */ | |
| if ( ! defined( 'ABSPATH' ) ) { | |
| exit; | |
| } | |
| final class WPMUDEV_Defender_CLI_Debug { | |
| private const OPTION_CLI = 'wd_cli_info'; | |
| private const OPTION_CRON = 'wd_cron_info'; | |
| private const OPTION_LOG = 'wd_defender_deactivate_log'; | |
| private const MAX_LOG = 20; | |
| private const DEFENDER_SLUGS = array( | |
| 'wp-defender/wp-defender.php', | |
| 'defender-security/wp-defender.php', | |
| ); | |
| /** @var self|null */ | |
| private static $instance = null; | |
| public static function get(): self { | |
| if ( null === self::$instance ) { | |
| self::$instance = new self(); | |
| } | |
| return self::$instance; | |
| } | |
| private function __construct() { | |
| // Capture every WP-CLI bootstrap (before / while plugins load). | |
| if ( defined( 'WP_CLI' ) && WP_CLI ) { | |
| $this->store_cli_snapshot( 'bootstrap' ); | |
| } | |
| // Same idea as the cron debug MU-plugin, kept for comparison. | |
| add_action( | |
| 'plugins_loaded', | |
| function () { | |
| if ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() ) { | |
| $this->store_cron_snapshot(); | |
| } | |
| }, | |
| 1 | |
| ); | |
| // Fires inside deactivate_plugins() — backtrace still includes the caller. | |
| add_action( 'deactivated_plugin', array( $this, 'on_deactivated_plugin' ), 0, 2 ); | |
| add_action( 'admin_menu', array( $this, 'admin_menu' ), 11 ); | |
| } | |
| /** | |
| * @param string $plugin Plugin basename. | |
| * @param bool $network_deactivating Network deactivation flag. | |
| */ | |
| public function on_deactivated_plugin( $plugin, $network_deactivating = false ): void { | |
| if ( ! in_array( (string) $plugin, self::DEFENDER_SLUGS, true ) ) { | |
| return; | |
| } | |
| $trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 40 ); | |
| $frames = array(); | |
| foreach ( $trace as $frame ) { | |
| $file = isset( $frame['file'] ) ? $frame['file'] : '[internal]'; | |
| $line = isset( $frame['line'] ) ? $frame['line'] : 0; | |
| $fn = ''; | |
| if ( ! empty( $frame['class'] ) ) { | |
| $fn = $frame['class'] . ( $frame['type'] ?? '::' ) . ( $frame['function'] ?? '' ); | |
| } else { | |
| $fn = $frame['function'] ?? ''; | |
| } | |
| $frames[] = sprintf( '%s() @ %s:%d', $fn, $file, $line ); | |
| } | |
| $entry = array( | |
| 'time' => gmdate( 'Y-m-d H:i:s' ) . ' UTC', | |
| 'plugin' => $plugin, | |
| 'network_deactivating' => (bool) $network_deactivating, | |
| 'php_version' => PHP_VERSION, | |
| 'php_sapi' => PHP_SAPI, | |
| 'php_binary' => defined( 'PHP_BINARY' ) ? PHP_BINARY : '', | |
| 'wp_cli' => defined( 'WP_CLI' ) && WP_CLI, | |
| 'wp_cli_version' => $this->wp_cli_version(), | |
| 'doing_cron' => function_exists( 'wp_doing_cron' ) && wp_doing_cron(), | |
| 'user_id' => function_exists( 'get_current_user_id' ) ? (int) get_current_user_id() : 0, | |
| 'request_uri' => isset( $_SERVER['REQUEST_URI'] ) ? (string) $_SERVER['REQUEST_URI'] : '', | |
| 'argv' => ( PHP_SAPI === 'cli' && isset( $GLOBALS['argv'] ) ) ? $GLOBALS['argv'] : array(), | |
| 'defender_constants' => array( | |
| 'DEFENDER_PLUGIN_BASENAME' => defined( 'DEFENDER_PLUGIN_BASENAME' ) ? DEFENDER_PLUGIN_BASENAME : null, | |
| 'WP_DEFENDER_PRO_PATH' => defined( 'WP_DEFENDER_PRO_PATH' ) ? WP_DEFENDER_PRO_PATH : null, | |
| 'WP_DEFENDER_MIN_PHP_VERSION'=> defined( 'WP_DEFENDER_MIN_PHP_VERSION' ) ? WP_DEFENDER_MIN_PHP_VERSION : null, | |
| 'WP_DEFENDER_PRO' => defined( 'WP_DEFENDER_PRO' ) ? WP_DEFENDER_PRO : null, | |
| 'DEFENDER_VERSION' => defined( 'DEFENDER_VERSION' ) ? DEFENDER_VERSION : null, | |
| ), | |
| 'active_plugins' => (array) get_option( 'active_plugins', array() ), | |
| 'call_chain' => $frames, | |
| ); | |
| $log = get_site_option( self::OPTION_LOG, array() ); | |
| if ( ! is_array( $log ) ) { | |
| $log = array(); | |
| } | |
| array_unshift( $log, $entry ); | |
| $log = array_slice( $log, 0, self::MAX_LOG ); | |
| update_site_option( self::OPTION_LOG, $log ); | |
| // Also mirror into error_log for hosts that only expose that. | |
| // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log | |
| error_log( '[Defender deactivate debug] ' . wp_json_encode( $entry ) ); | |
| } | |
| private function store_cli_snapshot( string $phase ): void { | |
| $info = array( | |
| 'phase' => $phase, | |
| 'time' => gmdate( 'Y-m-d H:i:s' ) . ' UTC', | |
| 'php_version' => PHP_VERSION, | |
| 'php_version_id' => PHP_VERSION_ID, | |
| 'php_sapi' => PHP_SAPI, | |
| 'php_binary' => defined( 'PHP_BINARY' ) ? PHP_BINARY : '', | |
| 'php_ini' => function_exists( 'php_ini_loaded_file' ) ? (string) php_ini_loaded_file() : '', | |
| 'wp_cli_version' => $this->wp_cli_version(), | |
| 'argv' => isset( $GLOBALS['argv'] ) ? $GLOBALS['argv'] : array(), | |
| 'user_id' => function_exists( 'get_current_user_id' ) ? (int) get_current_user_id() : 0, | |
| ); | |
| update_site_option( self::OPTION_CLI, $info ); | |
| } | |
| private function store_cron_snapshot(): void { | |
| update_option( | |
| self::OPTION_CRON, | |
| array( | |
| 'php_version' => PHP_VERSION, | |
| 'php_version_id' => PHP_VERSION_ID, | |
| 'php_sapi' => PHP_SAPI, | |
| 'php_binary' => defined( 'PHP_BINARY' ) ? PHP_BINARY : '', | |
| 'time' => gmdate( 'Y-m-d H:i:s' ) . ' UTC', | |
| 'server' => isset( $_SERVER ) ? $_SERVER : array(), | |
| ) | |
| ); | |
| } | |
| private function wp_cli_version(): string { | |
| if ( defined( 'WP_CLI_VERSION' ) ) { | |
| return (string) WP_CLI_VERSION; | |
| } | |
| if ( class_exists( '\WP_CLI' ) && defined( '\WP_CLI::VERSION' ) ) { | |
| return (string) \WP_CLI::VERSION; | |
| } | |
| return ''; | |
| } | |
| public function admin_menu(): void { | |
| add_menu_page( | |
| 'Defender CLI Debug', | |
| 'Defender CLI Debug', | |
| 'manage_options', | |
| 'wd_cli_debug', | |
| array( $this, 'admin_debug_page' ) | |
| ); | |
| } | |
| public function admin_debug_page(): void { | |
| if ( ! current_user_can( 'manage_options' ) ) { | |
| return; | |
| } | |
| $cli = get_site_option( self::OPTION_CLI ); | |
| $cron = get_option( self::OPTION_CRON ); | |
| $log = get_site_option( self::OPTION_LOG, array() ); | |
| $cli_php = is_array( $cli ) ? ( $cli['php_version'] ?? null ) : null; | |
| $cron_php = is_array( $cron ) ? ( $cron['php_version'] ?? null ) : null; | |
| $cli_ok = $cli_php ? version_compare( $cli_php, '8.0.0', '>=' ) : null; | |
| $cron_ok = $cron_php ? version_compare( $cron_php, '8.0.0', '>=' ) : null; | |
| ?> | |
| <div style="max-width:900px;background:#fff;border:1px solid #dadada;padding:0 20px 20px;margin-top:20px;"> | |
| <h1>Defender WP-CLI / deactivation debug</h1> | |
| <p>Must-use plugin. Records CLI/cron PHP snapshots and full call chains when Defender is deactivated.</p> | |
| <h2>WP-CLI snapshot</h2> | |
| <?php if ( empty( $cli ) ) : ?> | |
| <p><strong style="color:#df901c">Waiting for WP-CLI request</strong></p> | |
| <p>Run any WP-CLI command that loads WordPress, e.g. <code>wp plugin list</code>, then refresh.</p> | |
| <?php else : ?> | |
| <p> | |
| Status: | |
| <strong style="color:<?php echo $cli_ok ? 'green' : 'red'; ?>"> | |
| <?php echo $cli_ok ? 'Compatible' : 'Not compatible'; ?> | |
| </strong> | |
| </p> | |
| <p>CLI PHP: <strong><?php echo esc_html( (string) $cli_php ); ?></strong></p> | |
| <p>Binary: <code><?php echo esc_html( (string) ( $cli['php_binary'] ?? '' ) ); ?></code></p> | |
| <p>SAPI: <code><?php echo esc_html( (string) ( $cli['php_sapi'] ?? '' ) ); ?></code></p> | |
| <p>WP-CLI: <code><?php echo esc_html( (string) ( $cli['wp_cli_version'] ?? '' ) ); ?></code></p> | |
| <p>Last argv: <code><?php echo esc_html( wp_json_encode( $cli['argv'] ?? array() ) ); ?></code></p> | |
| <p>Captured: <code><?php echo esc_html( (string) ( $cli['time'] ?? '' ) ); ?></code></p> | |
| <?php endif; ?> | |
| <hr /> | |
| <h2>Cron snapshot (same check as Rev.2 cron debug)</h2> | |
| <?php if ( empty( $cron ) ) : ?> | |
| <p><strong style="color:#df901c">Waiting for cron request</strong></p> | |
| <?php else : ?> | |
| <p> | |
| Status: | |
| <strong style="color:<?php echo $cron_ok ? 'green' : 'red'; ?>"> | |
| <?php echo $cron_ok ? 'Compatible' : 'Not compatible'; ?> | |
| </strong> | |
| </p> | |
| <p>Cron PHP: <strong><?php echo esc_html( (string) $cron_php ); ?></strong></p> | |
| <p>Captured: <code><?php echo esc_html( (string) ( $cron['time'] ?? '' ) ); ?></code></p> | |
| <?php endif; ?> | |
| <hr /> | |
| <h2>Defender deactivation log</h2> | |
| <?php if ( empty( $log ) || ! is_array( $log ) ) : ?> | |
| <p>No Defender deactivations captured yet.</p> | |
| <?php else : ?> | |
| <?php foreach ( $log as $i => $entry ) : ?> | |
| <details <?php echo 0 === (int) $i ? 'open' : ''; ?> style="margin-bottom:12px;"> | |
| <summary> | |
| <strong><?php echo esc_html( (string) ( $entry['time'] ?? '' ) ); ?></strong> | |
| — | |
| <?php echo ! empty( $entry['wp_cli'] ) ? 'WP-CLI' : ( ! empty( $entry['doing_cron'] ) ? 'cron' : 'web' ); ?> | |
| — | |
| PHP <?php echo esc_html( (string) ( $entry['php_version'] ?? '' ) ); ?> | |
| </summary> | |
| <pre style="white-space:pre-wrap;background:#f6f7f7;padding:12px;overflow:auto;"><?php echo esc_html( wp_json_encode( $entry, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); ?></pre> | |
| </details> | |
| <?php endforeach; ?> | |
| <?php endif; ?> | |
| <hr /> | |
| <p style="margin-bottom:0"><strong>Note:</strong> Defender requires PHP ≥ 8.0.0. With CLI already on 8.4, the next deactivation’s <code>call_chain</code> is the important signal (PHP guard vs free/pro conflict vs something else).</p> | |
| </div> | |
| <?php | |
| } | |
| } | |
| WPMUDEV_Defender_CLI_Debug::get(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment