'PHP Error Log', 'path' => $php_log, ); } // WordPress debug.log, prefer relocated path set by quick-fix mu-plugin $wp_debug_log = csdt_devtools_wp_debug_log_path(); $sources['wp_debug'] = array( 'label' => 'WordPress Debug Log', 'path' => $wp_debug_log, ); // Web server error log, check common paths that may be readable by the web user $web_error_candidates = array( '/var/log/apache2/error.log', '/var/log/httpd/error_log', '/var/log/nginx/error.log', '/var/log/apache2/error_log', ); foreach ( $web_error_candidates as $path ) { if ( is_readable( $path ) ) { $sources['web_error'] = array( 'label' => 'Web Server Error Log', 'path' => $path, ); break; } } // Web server access log $web_access_candidates = array( '/var/log/apache2/access.log', '/var/log/httpd/access_log', '/var/log/nginx/access.log', ); foreach ( $web_access_candidates as $path ) { if ( is_readable( $path ) ) { $sources['web_access'] = array( 'label' => 'Web Server Access Log', 'path' => $path, ); break; } } // SSH auth log, readable if www-data is in the adm group $auth_log_candidates = array( '/var/log/auth.log', // Debian/Ubuntu '/var/log/secure', // RHEL/CentOS/Fedora '/var/log/messages', // some RHEL variants ); foreach ( $auth_log_candidates as $path ) { if ( is_readable( $path ) ) { $sources['auth_ssh'] = array( 'label' => 'SSH Auth Log', 'path' => $path, ); break; } } // WP Cron log, plugin-owned location under wp-content/uploads (via // get_log_dir()), not a hardcoded wp-content root path. Sites that write a // cron log elsewhere can add it through the custom log paths below. $cron_log = self::get_log_dir() . '/cron.log'; if ( file_exists( $cron_log ) ) { $sources['wp_cron'] = array( 'label' => 'WP Cron Log', 'path' => $cron_log, ); } // Admin-configured custom paths $custom = get_option( 'csdt_custom_log_paths', array() ); if ( is_array( $custom ) ) { foreach ( $custom as $i => $cp ) { // Re-validate at read time so a path saved by an older build or a // poisoned option can't be replayed to read wp-config.php / PHP source. if ( ! empty( $cp['label'] ) && ! empty( $cp['path'] ) && self::is_safe_log_path( (string) $cp['path'] ) ) { $sources[ 'custom_' . $i ] = array( 'label' => sanitize_text_field( $cp['label'] ), 'path' => $cp['path'], 'custom' => true, ); } } } return $sources; } public static function ajax_logs_setup_php(): void { check_ajax_referer( self::LOGS_NONCE, 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Unauthorized', 403 ); } try { $log_path = self::get_log_dir() . '/php-error.log'; // Store the path; CSDT_DevTools::init() relocates the PHP error log to it // at runtime via ini_set(). No must-use plugin is written (a WordPress.org // plugin may not deploy executable code into wp-content/mu-plugins). update_option( 'csdt_php_error_log_path', $log_path, false ); wp_send_json_success( array( 'path' => $log_path, 'sources' => self::get_log_sources(), ) ); } catch ( \Throwable $e ) { CSDT_Ajax::fail( $e, __CLASS__ . '::' . __FUNCTION__ ); } } public static function ajax_logs_custom_save(): void { // Choosing which server files the log viewer may read is a real administrator's // decision. A test session holds manage_options without a password or second factor. if ( self::viewer_is_test_session() ) { wp_send_json_error( 'This tool is not available to a test session.', 403 ); } check_ajax_referer( self::LOGS_NONCE, 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Unauthorized', 403 ); } try { $raw = isset( $_POST['paths'] ) ? sanitize_text_field( wp_unslash( $_POST['paths'] ) ) : '[]'; // A JSON list of file paths: no markup to keep. $paths = json_decode( $raw, true ); if ( ! is_array( $paths ) ) { $paths = array(); } $clean = array(); $rejected = array(); foreach ( $paths as $p ) { $label = sanitize_text_field( $p['label'] ?? '' ); $path = sanitize_text_field( $p['path'] ?? '' ); if ( $label === '' || $path === '' ) { continue; } // Reject paths that could expose PHP source / wp-config / secrets; // the viewer is for log files only. if ( ! self::is_safe_log_path( $path ) ) { $rejected[] = $path; continue; } $clean[] = array( 'label' => $label, 'path' => $path, ); } update_option( 'csdt_custom_log_paths', $clean ); wp_send_json_success( array( 'sources' => self::get_log_sources(), 'rejected' => $rejected, ) ); } catch ( \Throwable $e ) { CSDT_Ajax::fail( $e, __CLASS__ . '::' . __FUNCTION__ ); } } private static function render_debug_panel(): void { $has_key = CSDT_AI_Dispatcher::has_key(); $key_url = admin_url( 'admin.php?page=' . self::TOOLS_SLUG . '&tab=security' ); $perf_on = CSDT_Options::get( 'csdt_devtools_perf_monitor_enabled' ) !== '0'; ?>
define( 'SAVEQUERIES', true ); in wp-config.php, which a plugin may not switch on for you; the DB tab shows the line with a copy button. Remove it when you have finished, because it records every query on every request.',
),
array(
'name' => 'N+1 DB query detection',
'rec' => 'Info',
'html' => 'CS Monitor groups SQL queries by normalised pattern and flags any pattern that fires 3 or more times on a single page load as a potential N+1 issue. WP core patterns (options, post counts, site icon) are downgraded to info, they are eliminated by installing the persistent object cache below.',
),
array(
'name' => 'When to disable',
'rec' => 'Recommended',
'html' => 'Leave CS Monitor on during development and debugging. Turn it off on production pages that are publicly visited or performance-benchmarked, as the overlay JS adds a small overhead and its query tracking touches the same request lifecycle you are measuring.',
),
)
);
?>
wp-content/object-cache.php exists, and whose it is: Redis, Memcached, a CloudScale drop-in from an older version of this plugin, or another drop-in. WordPress loads that file on every request and uses it to keep database results (options, users, post counts) in memory between requests.',
),
array(
'name' => 'Why this plugin does not install one',
'rec' => 'Info',
'html' => 'An object cache has to replace WordPress core functions by their exact names, which a plugin in the WordPress.org directory may not ship. Install a dedicated object-cache plugin instead: Find an object-cache plugin opens the plugin directory search inside your own admin. Redis Object Cache is a common choice when your host provides Redis.',
),
array(
'name' => 'WP core N+1 patterns',
'rec' => 'Info',
'html' => 'WordPress core makes repeated DB calls for options, user data, post counts, and the site icon on every page load. These cannot be fixed in plugin code, they are inside WP core itself. A persistent object cache is the correct solution: WordPress checks the cache before hitting the DB, so the same record is only fetched once per cache lifetime.',
),
array(
'name' => 'Removing an older CloudScale drop-in',
'rec' => 'Recommended',
'html' => 'Older versions of this plugin offered an APCu drop-in. It is no longer maintained. If one is installed, Remove deletes wp-content/object-cache.php, and only when it is that CloudScale drop-in. Third-party drop-ins (Redis, Memcached) must be removed through their own plugin to prevent accidental data loss.',
),
)
);
?>
php-fpm status and are display-only.) High saturation causes slow page loads even when DB and server CPU are healthy.',
),
array(
'name' => 'Alert threshold',
'rec' => 'Recommended',
'html' => 'Leave the count at 1 while you are stabilising a site so every new error is reported, and raise it to 5-10 on a noisy production site to suppress bursts. Fatal and critical entries always alert regardless of the count, so raising it never hides an outage.',
),
)
);
?>
1 to be notified about every error. PHP Fatal and critical log entries always trigger an alert regardless of threshold.',
),
array(
'name' => 'Log paths',
'rec' => 'Recommended',
'html' => 'Watches the same log sources configured in the Server Logs panel. If PHP is logging to /dev/stderr (default in many Docker setups), errors are not readable here. Enable the mu-plugin under Server Logs → PHP Error Log to redirect errors to wp-content/php-error.log.',
),
)
);
?>
pm.status_path). Requires pm.status_path = /fpm-status in your www.conf and a matching nginx location block. Click Refresh at any time to re-poll.',
),
array(
'name' => 'Auto-restart',
'rec' => 'Optional',
'html' => 'When enabled, the script issues a docker restart {container} command after declaring saturation. A restart cooldown prevents thrashing. Use with care on production, a restart drops all in-flight requests.',
),
array(
'name' => 'Setup',
'rec' => 'Info',
'html' => 'Copy the settings from the Host-side monitor section below into whatever monitors PHP-FPM on your host. The callback URL and token wire it back to this panel so saturation events appear in the audit trail automatically.',
),
),
'Monitors PHP-FPM worker exhaustion from the host OS. Alerts via Telegram + email, can auto-restart the container, and logs events back to this panel.'
);
?>
* * * * * /path/to/your-fpm-monitor.sh 2>/dev/null
opcache.memory_consumption in php.ini if the cache is frequently full.',
),
)
);
?>
/dev/stderr (the default in many Docker/container setups), errors cannot be read here. Click Enable to install a mu-plugin that redirects PHP errors to wp-content/php-error.log. The mu-plugin runs on every request before other plugins load.',
),
array(
'name' => 'Filters',
'rec' => 'Info',
'html' => 'storage/logs/laravel.log), cron output files, or a custom PHP-FPM pool log. Labels are free-text, choose something descriptive. Custom paths are saved as a WordPress option and survive plugin updates.',
),
array(
'name' => 'Permissions',
'rec' => 'Info',
'html' => 'System logs (e.g. /var/log/syslog, /var/log/auth.log) are typically owned by root and not readable by www-data. This is intentional OS hardening, the plugin shows a clear "permission denied" message rather than an error. To expose a system log, add your web server user to the adm group or use a log-shipping tool.',
),
)
);
?>
type: bar, title: Revenue by quarter, x: Q1, Q2, Q3, then 2025: 12, 15, 17 and 2026: 19, 23, 26. Types are line, area, bar, hbar, stacked, pie and donut. Readers get a legend that hides or shows each series, tooltips, and the same Source and theme buttons. The chart library is bundled and loads only on pages with a chart.',
),
array(
'name' => 'Chart.js, Vega and Vega-Lite',
'rec' => 'Info',
'html' => 'A chart you already have as JSON can be pasted as it is. With the language set to Chart or Chart.js configuration, a block that starts with { is read as a Chart.js configuration (type, data, options), which is what AI assistants usually write. With the language set to Vega or Vega-Lite, the block is drawn from its specification. Colours you set are kept; anything left unset follows the light or dark theme. It must be plain JSON: no functions, comments or trailing commas. A Vega chart may load data files from this site only, so put the data in the specification or upload the file to the media library.',
),
array(
'name' => 'What readers get',
'rec' => 'Info',
'html' => 'The drawn diagram, with a Source button that shows the text, a Copy image button, and the light/dark toggle every code block has, which redraws the diagram to match. Copy image puts a picture of the diagram or chart on the clipboard, with its title and legend, ready to paste into a message or a slide. In the Source view the same button copies the text. A browser that cannot copy a picture copies the text and says so. A diagram with a mistake in it shows its source and the error instead of a blank box.',
),
array(
'name' => 'Performance and privacy',
'rec' => 'Info',
'html' => 'Mermaid ' . esc_html( self::MERMAID_VERSION ) . ', Chart.js ' . esc_html( self::CHARTJS_VERSION ) . ', Vega ' . esc_html( self::VEGA_VERSION ) . ' and Vega-Lite ' . esc_html( self::VEGA_LITE_VERSION ) . ' are bundled in the plugin. Each is loaded only on pages that contain a diagram or chart that needs it, so every other page pays nothing. No CDN is contacted, and Vega expressions are run by its interpreter rather than eval, so charts work under a strict Content Security Policy.',
),
array(
'name' => 'Safety',
'rec' => 'Info',
'html' => 'Diagrams render in Mermaid\'s strict security mode: labels are sanitised and click handlers or scripts in the diagram text are ignored, so a diagram cannot run code on your site.',
),
)
);
?>
```mermaid fence.', 'cloudscale-cyber-devtools' ),
array(
'strong' => array(),
'code' => array(),
)
);
?>
Chart for line, bar and pie charts. Explain has the format.', 'cloudscale-cyber-devtools' ), array( 'strong' => array() ) ); ?>
wp:code and wp:preformatted blocks, and blocks made by the Code Block Pro plugin.',
),
array(
'name' => 'Code Block Pro',
'rec' => 'Informational',
'html' => 'A Code Block Pro block is converted from the code its author typed, so the text is carried over exactly, indentation included. Its language is kept when this block highlights that language, and is otherwise left to be detected. A header title is kept. Its own colour theme, font, line numbers and line highlights are not carried over: the block takes the site theme set under Re-apply Default Theme. A block that holds no readable code is left untouched. Code Block Pro does not need to be installed or active for its blocks to be converted.',
),
array(
'name' => 'Preview',
'rec' => 'Recommended',
'html' => 'Shows a side-by-side before/after diff for each post before committing any changes, so you can review exactly what will be converted.',
),
array(
'name' => 'Migrate',
'rec' => 'Optional',
'html' => 'Converts detected legacy blocks to CloudScale format. Each post is saved with the converted markup.
array() ) ), '' . esc_html__( 'Scan Posts', 'cloudscale-cyber-devtools' ) . '' ); ?>
theme override so existing blocks snap to the new default too. Without this step, only new blocks pick up the change.',
),
array(
'name' => 'What it leaves alone',
'rec' => 'Informational',
'html' => 'Only the per-block theme attribute is reset. language, title, GitHub source URL, prefix/suffix, and all other per-block settings are not touched.',
),
array(
'name' => 'Per-block overrides',
'rec' => 'Informational',
'html' => 'After re-applying, any block can still set its own theme override from the block inspector, handy for a single demo block that should always render dark or always light regardless of the site default.',
),
array(
'name' => 'Reversibility',
'rec' => 'Warning',
'html' => 'There is no undo button. If you have many blocks with intentional per-block theme overrides you want to keep, take a database backup first.',
),
)
);
?>