get_version(); $assets_dir = charitable()->get_path( 'assets', false ); /* The following styles are only loaded on Charitable screens. */ $screen = get_current_screen(); if ( ( ! empty( $_GET['page'] ) && 'charitable-tools' === $_GET['page'] ) || ( ! is_null( $screen ) && 'charitable_page_charitable-tools' === $screen->base ) || ( ! empty( $_GET['tab'] ) && 'system-info' === $_GET['tab'] ) || ( isset( $_SERVER['REQUEST_URI'] ) && strpos( $_SERVER['REQUEST_URI'], 'charitable-tools' ) !== false ) ) { // phpcs:ignore wp_enqueue_style( 'charitable-system-info', $assets_dir . 'css/tools/system-info' . $min . '.css', array(), $version ); wp_enqueue_script( 'charitable-system-info', $assets_dir . "js/tools/system-info{$min}.js", [ 'jquery' ], $version, true ); // Localize script for AJAX wp_localize_script( 'charitable-system-info', 'charitable_email_diagnostics', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'charitable_email_diagnostics' ), 'test_email_nonce' => wp_create_nonce( 'charitable_send_test_email' ), 'running_text' => __( 'Running Diagnostics...', 'charitable' ), 'error_text' => __( 'Diagnostics failed. Please try again.', 'charitable' ), 'sending_text' => __( 'Sending...', 'charitable' ) ) ); // Localize script for admin functions (error logs, SSL, etc.) // Use unique variable 'charitable_admin_tools' so it is not overwritten by // charitable-admin-2.0.js which also localizes 'charitable_admin' (no nonce). // Nonce: wp_create_nonce( 'charitable-admin-tools' ), verified by check_ajax_referer( 'charitable-admin-tools', 'nonce', false ). wp_localize_script( 'charitable-system-info', 'charitable_admin_tools', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'charitable-admin-tools' ), 'testing' => __( 'Testing...', 'charitable' ), ) ); } } /** * Get view label. * * @since 1.8.1.6 * * @return string */ public function get_label() { return esc_html__( 'System Info', 'charitable' ); } /** * Checking user capability to view. * * @since 1.8.1.6 * * @return bool */ public function check_capability() { return charitable_current_user_can(); } /** * System view content. * * @since 1.8.1.6 */ public function display() { ?>

get_ui_state(); // Show status message based on UI state switch ( $ui_state['status'] ) { case 'disabled_no_file': echo '
'; echo '

' . esc_html__( 'WordPress Debug Logging Not Enabled', 'charitable' ) . '

'; echo '

' . esc_html__( 'To use the debug log scanner you need to enable WordPress debug logging. Add these lines to your wp-config.php file:', 'charitable' ) . '

'; echo '

define(\'WP_DEBUG\', true);
define(\'WP_DEBUG_LOG\', true);

'; echo '

' . sprintf( __( 'For more information see our guide to debugging WordPress', 'charitable' ), 'https://wpcharitable.com/documentation/debugging-wordpress' ) . '

'; echo '
'; break; case 'no_errors_yet': echo '
'; echo '

' . esc_html__( 'No Error Log Found', 'charitable' ) . '

'; echo '

' . esc_html( $ui_state['message'] ) . '

'; echo '
'; break; case 'disabled_but_file_exists': echo '
'; echo '

' . esc_html__( 'WordPress Debug Logging Disabled', 'charitable' ) . '

'; echo '

' . esc_html( $ui_state['message'] ) . '

'; echo '

' . sprintf( __( 'To enable debug logging see our guide to debugging WordPress', 'charitable' ), 'https://wpcharitable.com/documentation/debugging-wordpress' ) . '

'; echo '
'; break; } ?>

' . sprintf( esc_html( _n( '%d error found in recent logs', '%d errors found in recent logs', $error_count, 'charitable' ) ), $error_count ) . '

'; ?>

' . esc_html__( 'No recent donation form errors found.', 'charitable' ) . '

'; } ?>

charitable_info(); $data .= $this->site_info(); $data .= $this->wp_info(); $data .= $this->uploads_info(); $data .= $this->plugins_info(); $data .= $this->server_info(); $data .= $this->charitable_privacy_settings_info(); $data .= $this->email_diagnostics(); $data .= $this->donation_error_logs(); $data .= $this->debug_log_scanner(); $data .= $this->hosting_environment_info(); /** * Append extra System Info sections. * * Applied here, before the end marker, so a section cannot land * outside the block a customer copies out of the textarea. * * @since 1.8.13 * * @param string $sections Extra sections, already formatted. */ $data .= apply_filters( 'charitable_system_info_sections', '' ); $data .= "\n" . '### End System Info ###'; return $data; } /** * Get Charitable info. * * @since 1.8.1.6 * @version 1.8.3 added version upgraded from. * @version 1.8.4.5 * * @return string */ private function charitable_info() { $data = '-- Charitable Info' . "\n\n"; $activated = get_option( 'wpcharitable_activated_datetime', false ); $slug_dismissed = get_user_meta( get_current_user_id(), 'charitable-pointer-slug-dismissed', true ); $dismissed_addons = get_user_meta( get_current_user_id(), 'charitable_dismissed_addons', true ); $wpc_plugin_version = (array) get_site_option( 'wpc_plugin_version' ); $wpc_plugin_failure = (array) get_option( 'wpc_edd_sl_failed_plugin_versions' ); $versions_upgraded_from = get_option( 'charitable_version_upgraded_from', [] ); if ( ! empty( $versions_upgraded_from ) ) { $versions_upgraded_from = array_unique( $versions_upgraded_from ); } if ( ! empty( $versions_upgraded_from ) ) { $data .= 'Versions Upgraded From: ' . implode( ',', $versions_upgraded_from ) . "\n"; } if ( ! empty( $slug_dismissed ) ) { $data .= 'Pointer Slugs Dismissed: ' . implode( ',', $slug_dismissed ) . "\n"; } if ( ! empty( $dismissed_addons ) ) { $data .= 'G Removals: ' . implode( ',', $dismissed_addons ) . "\n"; } if ( ! empty( array_filter( $wpc_plugin_version ) ) ) { $data .= 'Plugin Version DEBUG 1: ' . implode( ',', $wpc_plugin_version ) . "\n"; } if ( ! empty( array_filter( $wpc_plugin_failure ) ) ) { $data .= 'Plugin Version DEBUG 2: ' . implode( ',', $wpc_plugin_failure ) . "\n"; } if ( $activated ) { $data .= 'Activated: ' . $this->get_formatted_datetime( $activated ) . "\n"; } return $data; } /** * Get Charitable privacy settings (for copyable system information). * * @since 1.8.1.6 * * @return string */ private function charitable_privacy_settings_info() { if ( ! function_exists( 'charitable_get_option' ) ) { return ''; } $retention_labels = array( 0 => __( 'None', 'charitable' ), 1 => __( 'One year', 'charitable' ), 2 => __( 'Two years', 'charitable' ), 3 => __( 'Three years', 'charitable' ), 4 => __( 'Four years', 'charitable' ), 5 => __( 'Five years', 'charitable' ), 6 => __( 'Six years', 'charitable' ), 7 => __( 'Seven years', 'charitable' ), 8 => __( 'Eight years', 'charitable' ), 9 => __( 'Nine years', 'charitable' ), 10 => __( 'Ten years', 'charitable' ), 'endless' => __( 'Forever', 'charitable' ), ); $retention_period = charitable_get_option( 'minimum_data_retention_period', 2 ); $retention_label = isset( $retention_labels[ $retention_period ] ) ? $retention_labels[ $retention_period ] : (string) $retention_period; $data = "\n" . '-- Charitable Privacy Settings' . "\n\n"; $data .= 'Minimum Data Retention: ' . $retention_label . "\n"; $retention_fields = charitable_get_option( 'data_retention_fields', array() ); if ( is_array( $retention_fields ) && ! empty( $retention_fields ) ) { $keys = array_keys( $retention_fields ); $list = array_values( $keys ) === range( 0, count( $keys ) - 1 ); $names = $list ? array_values( $retention_fields ) : $keys; $data .= 'Retained Data Fields: ' . implode( ', ', $names ) . "\n"; } else { $data .= 'Retained Data Fields: (none or all)' . "\n"; } $data .= 'Contact Consent Field: ' . ( charitable_get_option( 'contact_consent', 0 ) ? 'Enabled' : 'Disabled' ) . "\n"; $data .= 'Consent Required (Forms): ' . ( charitable_get_option( 'contact_consent_required', false ) ? 'Yes' : 'No' ) . "\n"; $contact_label_default = __( 'Yes, I am happy for you to contact me via email or phone.', 'charitable' ); $contact_label_value = charitable_get_option( 'contact_consent_label', $contact_label_default ); $data .= 'Contact Consent Label: ' . $this->privacy_text_summary( $contact_label_value, $contact_label_default ) . "\n"; $data .= 'Privacy Policy Field: ' . ( charitable_get_option( 'privacy_policy_enabled', 0 ) ? 'Enabled' : 'Disabled' ) . "\n"; $privacy_page_id = charitable_get_option( 'privacy_policy_page', 0 ); if ( ! empty( $privacy_page_id ) && get_post_status( $privacy_page_id ) ) { $data .= 'Privacy Policy Page: ' . get_the_title( $privacy_page_id ) . ' (#' . (int) $privacy_page_id . ')' . "\n"; } else { $data .= 'Privacy Policy Page: Not set' . "\n"; } $privacy_policy_default = __( 'Your personal data will be used to process your donation, support your experience throughout this website, and for other purposes described in our [privacy_policy].', 'charitable' ); $privacy_policy_value = charitable_get_option( 'privacy_policy', $privacy_policy_default ); $data .= 'Privacy Policy Text: ' . $this->privacy_text_summary( $privacy_policy_value, $privacy_policy_default ) . "\n"; $data .= 'Terms and Conditions Field: ' . ( charitable_get_option( 'terms_and_conditions_enabled', 0 ) ? 'Enabled' : 'Disabled' ) . "\n"; $terms_page_id = charitable_get_option( 'terms_conditions_page', 0 ); if ( ! empty( $terms_page_id ) && get_post_status( $terms_page_id ) ) { $data .= 'Terms and Conditions Page: ' . get_the_title( $terms_page_id ) . ' (#' . (int) $terms_page_id . ')' . "\n"; } else { $data .= 'Terms and Conditions Page: Not set' . "\n"; } $terms_default = __( 'I have read and agree to the website [terms].', 'charitable' ); $terms_value = charitable_get_option( 'terms_conditions', $terms_default ); $data .= 'Terms and Conditions Text: ' . $this->privacy_text_summary( $terms_value, $terms_default ) . "\n"; return $data; } /** * Short summary for a privacy text option: "Default" or "Custom (X characters)". * * @since 1.8.1.6 * * @param string $value Current option value. * @param string $default Default text to compare against. * @return string */ private function privacy_text_summary( $value, $default ) { $value = is_string( $value ) ? trim( $value ) : ''; $default = is_string( $default ) ? trim( $default ) : ''; if ( $value === $default || $value === '' ) { return 'Default'; } return 'Custom (' . strlen( $value ) . ' characters)'; } /** * Get Site info. * * @since 1.8.1.6 * * @return string */ private function site_info() { $data = "\n" . '-- Site Info' . "\n\n"; $data .= 'Site URL: ' . site_url() . "\n"; $data .= 'Home URL: ' . home_url() . "\n"; $data .= 'Multisite: ' . ( is_multisite() ? 'Yes' : 'No' ) . "\n"; return $data; } /** * Get WordPress Configuration info. * * @since 1.8.1.6 * * @return string */ private function wp_info() { global $wpdb; $theme_data = wp_get_theme(); $theme = $theme_data->name . ' ' . $theme_data->version; $data = "\n" . '-- WordPress Configuration' . "\n\n"; $data .= 'Version: ' . get_bloginfo( 'version' ) . "\n"; $data .= 'Language: ' . get_locale() . "\n"; $data .= 'User Language: ' . get_user_locale() . "\n"; $data .= 'Permalink Structure: ' . ( get_option( 'permalink_structure' ) ? get_option( 'permalink_structure' ) : 'Default' ) . "\n"; $data .= 'Active Theme: ' . $theme . "\n"; $data .= 'Show On Front: ' . get_option( 'show_on_front' ) . "\n"; // Only show page specs if front page is set to 'page'. if ( get_option( 'show_on_front' ) === 'page' ) { $front_page_id = get_option( 'page_on_front' ); $blog_page_id = get_option( 'page_for_posts' ); $data .= 'Page On Front: ' . ( $front_page_id ? get_the_title( $front_page_id ) . ' (#' . $front_page_id . ')' : 'Unset' ) . "\n"; $data .= 'Page For Posts: ' . ( $blog_page_id ? get_the_title( $blog_page_id ) . ' (#' . $blog_page_id . ')' : 'Unset' ) . "\n"; } $data .= 'ABSPATH: ' . ABSPATH . "\n"; $data .= 'Table Prefix: ' . 'Length: ' . strlen( $wpdb->prefix ) . ' Status: ' . ( strlen( $wpdb->prefix ) > 16 ? 'ERROR: Too long' : 'Acceptable' ) . "\n"; //phpcs:ignore $data .= 'WP_DEBUG: ' . ( defined( 'WP_DEBUG' ) ? WP_DEBUG ? 'Enabled' : 'Disabled' : 'Not set' ) . "\n"; $data .= 'WP_DEBUG_LOG: ' . ( defined( 'WP_DEBUG_LOG' ) ? ( WP_DEBUG_LOG ? ( is_string( WP_DEBUG_LOG ) ? 'Enabled (Custom: ' . WP_DEBUG_LOG . ')' : 'Enabled' ) : 'Disabled' ) : 'Not set' ) . "\n"; $data .= 'WP_DEBUG_DISPLAY: ' . ( defined( 'WP_DEBUG_DISPLAY' ) ? WP_DEBUG_DISPLAY ? 'Enabled' : 'Disabled' : 'Not set' ) . "\n"; $data .= 'SCRIPT_DEBUG: ' . ( defined( 'SCRIPT_DEBUG' ) ? SCRIPT_DEBUG ? 'Enabled' : 'Disabled' : 'Not set' ) . "\n"; $data .= 'CHARITABLE_DEBUG: ' . ( defined( 'CHARITABLE_DEBUG' ) ? CHARITABLE_DEBUG ? 'Enabled' : 'Disabled' : 'Not set' ) . "\n"; $data .= 'CHARITABLE_DEBUG_SECURITY: ' . ( defined( 'CHARITABLE_DEBUG_SECURITY' ) ? CHARITABLE_DEBUG_SECURITY ? 'Enabled' : 'Disabled' : 'Not set' ) . "\n"; $data .= 'Memory Limit: ' . WP_MEMORY_LIMIT . "\n"; $data .= 'Registered Post Stati: ' . implode( ', ', get_post_stati() ) . "\n"; $data .= 'Revisions: ' . ( WP_POST_REVISIONS ? WP_POST_REVISIONS > 1 ? 'Limited to ' . WP_POST_REVISIONS : 'Enabled' : 'Disabled' ) . "\n"; return $data; } /** * Get Uploads/Constants info. * * @since 1.8.1.6 * * @return string */ private function uploads_info() { // @todo Charitable configuration/specific details. $data = "\n" . '-- WordPress Uploads/Constants' . "\n\n"; $data .= 'WP_CONTENT_DIR: ' . ( defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR ? WP_CONTENT_DIR : 'Disabled' : 'Not set' ) . "\n"; $data .= 'WP_CONTENT_URL: ' . ( defined( 'WP_CONTENT_URL' ) ? WP_CONTENT_URL ? WP_CONTENT_URL : 'Disabled' : 'Not set' ) . "\n"; $data .= 'UPLOADS: ' . ( defined( 'UPLOADS' ) ? UPLOADS ? UPLOADS : 'Disabled' : 'Not set' ) . "\n"; $uploads_dir = wp_upload_dir(); $data .= 'wp_uploads_dir() path: ' . $uploads_dir['path'] . "\n"; $data .= 'wp_uploads_dir() url: ' . $uploads_dir['url'] . "\n"; $data .= 'wp_uploads_dir() basedir: ' . $uploads_dir['basedir'] . "\n"; $data .= 'wp_uploads_dir() baseurl: ' . $uploads_dir['baseurl'] . "\n"; return $data; } /** * Get Plugins info. * * @since 1.8.1.6 * * @return string */ private function plugins_info() { // Get plugins that have an update. $data = $this->mu_plugins(); $data .= $this->installed_plugins(); $data .= $this->multisite_plugins(); return $data; } /** * Get MU Plugins info. * * @since 1.8.1.6 * * @return string */ private function mu_plugins() { $data = ''; // Must-use plugins. // NOTE: MU plugins can't show updates! $muplugins = get_mu_plugins(); if ( ! empty( $muplugins ) && count( $muplugins ) > 0 ) { $data = "\n" . '-- Must-Use Plugins' . "\n\n"; foreach ( $muplugins as $plugin => $plugin_data ) { $data .= $plugin_data['Name'] . ': ' . $plugin_data['Version'] . "\n"; } } return $data; } /** * Get Installed Plugins info. * * @since 1.8.1.6 * * @return string */ private function installed_plugins() { $updates = function_exists( 'get_plugin_updates' ) ? get_plugin_updates() : array(); // WordPress active plugins. $data = "\n" . '-- WordPress Active Plugins' . "\n\n"; $plugins = get_plugins(); $active_plugins = get_option( 'active_plugins', [] ); foreach ( $plugins as $plugin_path => $plugin ) { if ( ! in_array( $plugin_path, $active_plugins, true ) ) { continue; } $update = ( array_key_exists( $plugin_path, $updates ) ) ? ' (needs update - ' . $updates[ $plugin_path ]->update->new_version . ')' : ''; $data .= $plugin['Name'] . ': ' . $plugin['Version'] . $update . "\n"; } // WordPress inactive plugins. $data .= "\n" . '-- WordPress Inactive Plugins' . "\n\n"; foreach ( $plugins as $plugin_path => $plugin ) { if ( in_array( $plugin_path, $active_plugins, true ) ) { continue; } $update = ( array_key_exists( $plugin_path, $updates ) ) ? ' (needs update - ' . $updates[ $plugin_path ]->update->new_version . ')' : ''; $data .= $plugin['Name'] . ': ' . $plugin['Version'] . $update . "\n"; } return $data; } /** * Get Multisite Plugins info. * * @since 1.8.1.6 * * @return string */ private function multisite_plugins() { $data = ''; if ( ! is_multisite() ) { return $data; } $updates = function_exists( 'get_plugin_updates' ) ? get_plugin_updates() : array(); // WordPress Multisite active plugins. $data = "\n" . '-- Network Active Plugins' . "\n\n"; $plugins = wp_get_active_network_plugins(); $active_plugins = get_site_option( 'active_sitewide_plugins', [] ); foreach ( $plugins as $plugin_path ) { $plugin_base = plugin_basename( $plugin_path ); if ( ! array_key_exists( $plugin_base, $active_plugins ) ) { continue; } $update = ( array_key_exists( $plugin_path, $updates ) ) ? ' (needs update - ' . $updates[ $plugin_path ]->update->new_version . ')' : ''; $plugin = get_plugin_data( $plugin_path ); $data .= $plugin['Name'] . ': ' . $plugin['Version'] . $update . "\n"; } return $data; } /** * Get Server info. * * @since 1.8.1.6 * * @return string */ private function server_info() { global $wpdb; // Server configuration (really just versions). $data = "\n" . '-- Webserver Configuration' . "\n\n"; $data .= 'PHP Version: ' . PHP_VERSION . "\n"; $data .= 'MySQL Version: ' . $wpdb->db_version() . "\n"; $data .= 'Webserver Info: ' . ( isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '' ) . "\n"; // PHP configs... now we're getting to the important stuff. $data .= "\n" . '-- PHP Configuration' . "\n\n"; $data .= 'Memory Limit: ' . ini_get( 'memory_limit' ) . "\n"; $data .= 'Upload Max Size: ' . ini_get( 'upload_max_filesize' ) . "\n"; $data .= 'Post Max Size: ' . ini_get( 'post_max_size' ) . "\n"; $data .= 'Upload Max Filesize: ' . ini_get( 'upload_max_filesize' ) . "\n"; $data .= 'Time Limit: ' . ini_get( 'max_execution_time' ) . "\n"; $data .= 'Max Input Vars: ' . ini_get( 'max_input_vars' ) . "\n"; $data .= 'Display Errors: ' . ( ini_get( 'display_errors' ) ? 'On (' . ini_get( 'display_errors' ) . ')' : 'N/A' ) . "\n"; // PHP extensions and such. $data .= "\n" . '-- PHP Extensions' . "\n\n"; $data .= 'cURL: ' . ( function_exists( 'curl_init' ) ? 'Supported' : 'Not Supported' ) . "\n"; $data .= 'fsockopen: ' . ( function_exists( 'fsockopen' ) ? 'Supported' : 'Not Supported' ) . "\n"; $data .= 'SOAP Client: ' . ( class_exists( 'SoapClient', false ) ? 'Installed' : 'Not Installed' ) . "\n"; $data .= 'Suhosin: ' . ( extension_loaded( 'suhosin' ) ? 'Installed' : 'Not Installed' ) . "\n"; // Session stuff. $data .= "\n" . '-- Session Configuration' . "\n\n"; $data .= 'Session: ' . ( isset( $_SESSION ) ? 'Enabled' : 'Disabled' ) . "\n"; // The rest of this is only relevant if session is enabled. if ( isset( $_SESSION ) ) { $data .= 'Session Name: ' . esc_html( ini_get( 'session.name' ) ) . "\n"; $data .= 'Cookie Path: ' . esc_html( ini_get( 'session.cookie_path' ) ) . "\n"; $data .= 'Save Path: ' . esc_html( ini_get( 'session.save_path' ) ) . "\n"; $data .= 'Use Cookies: ' . ( ini_get( 'session.use_cookies' ) ? 'On' : 'Off' ) . "\n"; $data .= 'Use Only Cookies: ' . ( ini_get( 'session.use_only_cookies' ) ? 'On' : 'Off' ) . "\n"; } return $data; } /** * Get formatted datetime. * * @since 1.8.5 * * @param int|string $date Date. * * @return string */ private function get_formatted_datetime( $date ) { return sprintf( '%1$s at %2$s (GMT)', gmdate( 'M j, Y', $date ), gmdate( 'g:ia', $date ) ); } /** * Get email diagnostics summary for system info. * * @since 1.8.9.2 * * @return string */ private function email_diagnostics() { try { $diagnostics = Charitable_Tools_Email_Diagnostics::get_instance(); return $diagnostics->get_email_diagnostics_summary(); } catch ( Exception $e ) { return "\n-- Email System Diagnostics\n\nError: Unable to run email diagnostics - " . $e->getMessage() . "\n"; } } /** * AJAX handler for detailed email diagnostics. * * @since 1.8.9.2 * * @return void */ public function ajax_email_diagnostics() { // Check nonce if ( ! wp_verify_nonce( $_POST['nonce'], 'charitable_email_diagnostics' ) ) { wp_die( __( 'Security check failed', 'charitable' ) ); } // Check capability if ( ! current_user_can( 'manage_charitable_settings' ) ) { wp_die( __( 'Insufficient permissions', 'charitable' ) ); } try { $diagnostics = Charitable_Tools_Email_Diagnostics::get_instance(); $results = $diagnostics->get_detailed_email_diagnostics(); // Format for display $output = $this->format_detailed_diagnostics( $results ); wp_send_json_success( array( 'diagnostics' => $output, 'raw_data' => $results ) ); } catch ( Exception $e ) { wp_send_json_error( array( 'message' => 'Diagnostics failed: ' . $e->getMessage(), 'error_details' => array( 'file' => $e->getFile(), 'line' => $e->getLine() ) ) ); } } /** * Format detailed diagnostics for display. * * @since 1.8.9.2 * * @param array $results Raw diagnostic results. * @return string Formatted diagnostic output. */ private function format_detailed_diagnostics( $results ) { if ( isset( $results['error'] ) ) { return "### DIAGNOSTIC ERROR ###\n\n" . $results['error']['message'] . "\n"; } $output = "### DETAILED EMAIL DIAGNOSTICS ###\n\n"; $output .= sprintf( "Generated: %s\n", $results['timestamp'] ?? 'Unknown' ); $output .= sprintf( "Execution Time: %sms\n\n", $results['execution_time'] ?? '0' ); // Health Score Section if ( isset( $results['health_score'] ) ) { $health = $results['health_score']; $output .= "=== OVERALL HEALTH SCORE ===\n"; $output .= sprintf( "Score: %d/%d (%d%%) - %s\n", $health['total_score'], $health['max_score'], $health['percentage'], $health['grade'] ); $output .= sprintf( "Assessment: %s\n\n", $health['interpretation'] ); // Category breakdown $output .= "Category Scores:\n"; foreach ( $health['category_scores'] as $category => $score ) { $output .= sprintf( " %-20s: %2d/%-2d (%3d%%)\n", ucwords( str_replace( '_', ' ', $category ) ), $score['score'], $score['max_score'], $score['percentage'] ); } $output .= "\n"; } // Individual Test Results $test_sections = array( 'email_registration' => 'EMAIL REGISTRATION TEST', 'email_access' => 'EMAIL ACCESS TEST', 'preview_functionality' => 'PREVIEW FUNCTIONALITY TEST', 'hook_timing' => 'HOOK TIMING TEST', 'environment_config' => 'ENVIRONMENT CONFIGURATION', 'performance' => 'PERFORMANCE METRICS' ); foreach ( $test_sections as $key => $title ) { if ( ! isset( $results[ $key ] ) ) { continue; } $test = $results[ $key ]; $output .= "=== {$title} ===\n"; $output .= sprintf( "Status: %s (Score: %d/%d)\n", strtoupper( $test['status'] ), $test['score'], $test['max_score'] ); // Test-specific details switch ( $key ) { case 'email_registration': $this->format_registration_details( $test, $output ); break; case 'email_access': $this->format_access_details( $test, $output ); break; case 'preview_functionality': $this->format_preview_details( $test, $output ); break; case 'hook_timing': $this->format_timing_details( $test, $output ); break; case 'environment_config': $this->format_environment_details( $test, $output ); break; case 'performance': $this->format_performance_details( $test, $output ); break; } $output .= "\n"; } $output .= "### END DETAILED DIAGNOSTICS ###"; return $output; } /** * Format registration test details. * * @since 1.8.9.2 * * @param array $test Test results. * @param string &$output Output string to append to. */ private function format_registration_details( $test, &$output ) { $details = $test['details']; $output .= sprintf( "Emails Registered: %d\n", count( $test['registered_emails'] ?? array() ) ); $output .= sprintf( "Core Emails Present: %d/%d\n", $details['core_emails_present'] ?? 0, $details['core_emails_total'] ?? 3 ); $output .= sprintf( "Registration Time: %sms\n", $test['timing']['duration_ms'] ?? '0' ); if ( isset( $details['helper_accessible'] ) && ! $details['helper_accessible'] ) { $output .= "⚠ Helper Access: FAILED\n"; if ( isset( $details['helper_error'] ) ) { $output .= " Error: " . $details['helper_error'] . "\n"; } } if ( ! empty( $test['registered_emails'] ) ) { $output .= "Registered Email Classes:\n"; foreach ( $test['registered_emails'] as $id => $class ) { $output .= " {$id}: {$class}\n"; } } } /** * Format access test details. * * @since 1.8.9.2 * * @param array $test Test results. * @param string &$output Output string to append to. */ private function format_access_details( $test, &$output ) { $details = $test['details']; $output .= sprintf( "Successful Access: %d/%d\n", $details['successful_accesses'] ?? 0, $details['total_emails_tested'] ?? 3 ); $output .= sprintf( "Average Access Time: %.1fms\n", $test['average_time'] ?? 0 ); if ( isset( $test['email_tests'] ) ) { $output .= "Individual Email Tests:\n"; foreach ( $test['email_tests'] as $email_id => $email_test ) { $status = $email_test['accessible'] ? '✓' : '✗'; $output .= sprintf( " %s %-25s: %s (%.1fms)\n", $status, $email_id, $email_test['accessible'] ? 'OK' : 'FAILED', $email_test['timing_ms'] ); if ( ! $email_test['accessible'] && isset( $email_test['access_error'] ) ) { $output .= " Error: " . $email_test['access_error'] . "\n"; } } } } /** * Format preview test details. * * @since 1.8.9.2 * * @param array $test Test results. * @param string &$output Output string to append to. */ private function format_preview_details( $test, &$output ) { $output .= sprintf( "Functional Previews: %d/%d\n", $test['functional_count'] ?? 0, $test['details']['total_tested'] ?? 3 ); if ( isset( $test['preview_tests'] ) ) { $output .= "Preview URL Tests:\n"; foreach ( $test['preview_tests'] as $email_id => $preview_test ) { $status = $preview_test['preview_works'] ? '✓' : '✗'; $output .= sprintf( " %s %-25s: %s", $status, $email_id, $preview_test['preview_works'] ? 'Working' : 'Failed' ); if ( isset( $preview_test['status_code'] ) ) { $output .= sprintf( " (HTTP %d)", $preview_test['status_code'] ); } $output .= "\n"; if ( ! $preview_test['preview_works'] && isset( $preview_test['endpoint_error'] ) ) { $output .= " Error: " . $preview_test['endpoint_error'] . "\n"; } } // If all failures are the local SSL certificate error, add a short explanation. $ssl_error_phrase = 'unable to get local issuer certificate'; $has_ssl_error = false; $any_failure = false; foreach ( $test['preview_tests'] as $preview_test ) { if ( ! $preview_test['preview_works'] ) { $any_failure = true; $err = isset( $preview_test['endpoint_error'] ) ? $preview_test['endpoint_error'] : ''; if ( strpos( $err, $ssl_error_phrase ) !== false || strpos( $err, 'cURL error 60' ) !== false ) { $has_ssl_error = true; break; } } } if ( $any_failure && $has_ssl_error ) { $output .= "\n Note: This is an SSL verification error. The server's PHP/cURL cannot verify your site's HTTPS certificate (common with local/dev or self-signed certs). It is not a Charitable bug; preview links may still work in the browser.\n"; } } } /** * Format timing test details. * * @since 1.8.9.2 * * @param array $test Test results. * @param string &$output Output string to append to. */ private function format_timing_details( $test, &$output ) { $details = $test['details']; $output .= sprintf( "Init Completed: %s\n", $details['init_completed'] ? 'Yes' : 'No' ); $output .= sprintf( "Admin Init Completed: %s\n", $details['admin_init_completed'] ? 'Yes' : 'No' ); $output .= sprintf( "Current Action: %s\n", $details['current_action'] ?? 'none' ); if ( isset( $details['charitable_plugin_position'] ) ) { $output .= sprintf( "Plugin Load Position: %d/%d\n", $details['charitable_plugin_position'] + 1, $details['total_active_plugins'] ); } if ( ! empty( $details['timing_issues'] ) ) { $output .= "Timing Issues:\n"; foreach ( $details['timing_issues'] as $issue ) { $output .= " ⚠ {$issue}\n"; } } } /** * Format environment test details. * * @since 1.8.9.2 * * @param array $test Test results. * @param string &$output Output string to append to. */ private function format_environment_details( $test, &$output ) { $env = $test['environment_info']; $output .= sprintf( "PHP Version: %s\n", $env['php_version'] ); $output .= sprintf( "WordPress Version: %s\n", $env['wordpress_version'] ); $output .= sprintf( "Charitable Version: %s\n", $env['charitable_version'] ); $output .= sprintf( "Memory Limit: %s\n", $env['memory_limit'] ); $output .= sprintf( "Debug Mode: %s\n", $env['debug_enabled'] ? 'Enabled' : 'Disabled' ); if ( ! empty( $test['caching_plugins'] ) ) { $output .= "Caching Plugins: " . implode( ', ', $test['caching_plugins'] ) . "\n"; } $output .= sprintf( "Object Cache: %s\n", $test['details']['object_cache_enabled'] ? 'Enabled' : 'Disabled' ); $output .= sprintf( "OPcache: %s\n", $test['details']['opcache_enabled'] ? 'Enabled' : 'Disabled' ); $output .= sprintf( "SMTP Configured: %s\n", $test['details']['smtp_configured'] ? 'Yes' : 'No' ); } /** * Format performance test details. * * @since 1.8.9.2 * * @param array $test Test results. * @param string &$output Output string to append to. */ private function format_performance_details( $test, &$output ) { $metrics = $test['metrics']; $output .= sprintf( "Helper Access Time: %.2fms\n", $metrics['helper_access_time_ms'] ); $output .= sprintf( "Registration Time: %.2fms\n", $metrics['registration_time_ms'] ); $output .= sprintf( "Total Test Time: %.2fms\n", $metrics['total_time_ms'] ); $output .= sprintf( "Memory Used: %.1fKB\n", $metrics['memory_used_kb'] ); $output .= sprintf( "Peak Memory: %.1fMB\n", $metrics['peak_memory_mb'] ); if ( isset( $test['details']['slow_helper'] ) ) { $output .= "⚠ " . $test['details']['slow_helper'] . "\n"; } if ( isset( $test['details']['slow_registration'] ) ) { $output .= "⚠ " . $test['details']['slow_registration'] . "\n"; } if ( isset( $test['details']['high_memory'] ) ) { $output .= "⚠ " . $test['details']['high_memory'] . "\n"; } } /** * AJAX handler for sending test email. * * @since 1.8.9.2 * * @return void */ public function ajax_send_test_email() { // Check nonce if ( ! wp_verify_nonce( $_POST['nonce'], 'charitable_send_test_email' ) ) { wp_die( __( 'Security check failed', 'charitable' ) ); } // Check capability if ( ! current_user_can( 'manage_charitable_settings' ) ) { wp_die( __( 'Insufficient permissions', 'charitable' ) ); } try { // Load the test email class if not already loaded if ( ! class_exists( 'Charitable_Email_Test' ) ) { require_once charitable()->get_path( 'includes' ) . 'emails/class-charitable-email-test.php'; } // Send test email $result = Charitable_Email_Test::send_test_email(); if ( $result['success'] ) { wp_send_json_success( array( 'message' => $result['message'], 'success' => true, ) ); } else { wp_send_json_error( array( 'message' => $result['message'], 'success' => false, ) ); } } catch ( Exception $e ) { wp_send_json_error( array( 'message' => sprintf( __( 'Test email failed: %s', 'charitable' ), $e->getMessage() ), 'success' => false, 'error_details' => array( 'file' => $e->getFile(), 'line' => $e->getLine(), ), ) ); } } /** * Get donation form error logs. * * @since 1.8.9.2 * * @return string */ private function donation_error_logs() { $data = "\n\n" . '-- Donation Form Error Logs' . "\n\n"; // Check if error logging is enabled if ( ! apply_filters( 'charitable_form_error_logging_enabled', true ) ) { $data .= 'Error logging is disabled.' . "\n"; return $data; } // Get recent errors (last 20) $recent_errors = charitable_get_recent_form_errors( 20 ); if ( empty( $recent_errors ) ) { $data .= 'No donation form errors found in the last 7 days.' . "\n"; return $data; } $data .= 'Recent Errors (last 20):' . "\n"; $data .= str_repeat( '-', 40 ) . "\n"; foreach ( $recent_errors as $error ) { $data .= sprintf( '[%s] %s - %s', $error->date_recorded, $error->primary_action, $error->secondary_action ); // Add context if available if ( $error->campaign_id ) { $campaign_title = get_the_title( $error->campaign_id ); $data .= sprintf( ' | Campaign: %s (#%d)', $campaign_title, $error->campaign_id ); } if ( $error->amount ) { $data .= sprintf( ' | Amount: %s', charitable_format_money( $error->amount ) ); } if ( ! empty( $error->meta_data ) && isset( $error->meta_data['gateway'] ) ) { $data .= sprintf( ' | Gateway: %s', $error->meta_data['gateway'] ); } // Show error message detail (SMTP errors, exception messages, etc.) if ( ! empty( $error->meta_data ) && isset( $error->meta_data['error_message'] ) ) { $data .= "\n → " . $error->meta_data['error_message']; } // Show email class for email failures if ( ! empty( $error->meta_data ) && isset( $error->meta_data['email_class'] ) ) { $data .= ' [' . $error->meta_data['email_class'] . ']'; } $data .= "\n"; } // Get error statistics $error_stats = charitable_get_form_error_stats( 7 ); if ( ! empty( $error_stats ) ) { $data .= "\n" . 'Error Summary (last 7 days):' . "\n"; $data .= str_repeat( '-', 40 ) . "\n"; foreach ( $error_stats as $stat ) { $data .= sprintf( '%s - %s: %d occurrences (last: %s)' . "\n", $stat->primary_action, $stat->secondary_action, $stat->error_count, $stat->last_occurrence ); } } return $data; } /** * Get debug log scanner summary for system info. * * @since 1.8.9.2 * * @return string */ private function debug_log_scanner() { try { $scanner = Charitable_Tools_Debug_Log_Scanner::get_instance(); return $scanner->get_debug_log_scanner_summary(); } catch ( Exception $e ) { return "\n-- Debug Log Scanner\n\nError: Unable to run debug log scanner - " . $e->getMessage() . "\n"; } } /** * Clear error logs via AJAX. * * @since 1.8.9.2 * * @return void */ public function ajax_clear_error_logs() { // Check nonce first (distinct message for debugging). if ( ! check_ajax_referer( 'charitable-admin-tools', 'nonce', false ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed: invalid or expired nonce. Refresh the page and try again.', 'charitable' ), 'success' => false, ) ); return; } // Check capability. if ( ! current_user_can( 'manage_charitable_settings' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have the manage_charitable_settings capability.', 'charitable' ), 'success' => false, ) ); return; } try { // Clear error logs (delete all) $deleted_count = charitable_cleanup_old_form_errors( 0 ); // 0 days = delete all wp_send_json_success( array( 'message' => sprintf( /* translators: %d: number of deleted error logs */ _n( '%d error log cleared successfully.', '%d error logs cleared successfully.', $deleted_count, 'charitable' ), $deleted_count ), 'success' => true, 'deleted_count' => $deleted_count, ) ); } catch ( Exception $e ) { wp_send_json_error( array( 'message' => sprintf( __( 'Error clearing logs: %s', 'charitable' ), $e->getMessage() ), 'success' => false, ) ); } } /** * Export error logs via AJAX. * * @since 1.8.9.2 * * @return void */ public function ajax_export_error_logs() { // Check nonce first (distinct message for debugging). if ( ! check_ajax_referer( 'charitable-admin-tools', 'nonce', false ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed: invalid or expired nonce. Refresh the page and try again.', 'charitable' ), 'success' => false, ) ); return; } // Check capability. if ( ! current_user_can( 'manage_charitable_settings' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have the manage_charitable_settings capability.', 'charitable' ), 'success' => false, ) ); return; } try { // Get all error logs $errors = charitable_get_recent_form_errors( 1000 ); // Get up to 1000 errors $csv_data = array(); $csv_data[] = array( 'Date', 'Error Type', 'Error Details', 'Campaign ID', 'Campaign Title', 'Amount', 'Gateway', 'User ID' ); foreach ( $errors as $error ) { $campaign_title = $error->campaign_id ? get_the_title( $error->campaign_id ) : ''; $gateway = ! empty( $error->meta_data ) && isset( $error->meta_data['gateway'] ) ? $error->meta_data['gateway'] : ''; $csv_data[] = array( $error->date_recorded, $error->primary_action, $error->secondary_action, $error->campaign_id ?: '', $campaign_title, $error->amount ? charitable_format_money( $error->amount ) : '', $gateway, $error->created_by ?: '', ); } // Create CSV content $csv_content = ''; foreach ( $csv_data as $row ) { $csv_content .= '"' . implode( '","', $row ) . '"' . "\n"; } // Prepare download data $filename = 'charitable-error-logs-' . gmdate( 'Y-m-d-H-i-s' ) . '.csv'; wp_send_json_success( array( 'message' => __( 'Error logs exported successfully.', 'charitable' ), 'success' => true, 'filename' => $filename, 'content' => base64_encode( $csv_content ), 'mimetype' => 'text/csv', ) ); } catch ( Exception $e ) { wp_send_json_error( array( 'message' => sprintf( __( 'Error exporting logs: %s', 'charitable' ), $e->getMessage() ), 'success' => false, ) ); } } /** * AJAX handler for debug log scanner. * * @since 1.8.9.2 * * @return void */ public function ajax_debug_log_scan() { // Check nonce if ( ! wp_verify_nonce( $_POST['nonce'], 'charitable-admin-tools' ) ) { wp_die( __( 'Security check failed', 'charitable' ) ); } // Check capability if ( ! current_user_can( 'manage_charitable_settings' ) ) { wp_die( __( 'Insufficient permissions', 'charitable' ) ); } try { $scanner = Charitable_Tools_Debug_Log_Scanner::get_instance(); $results = $scanner->get_detailed_debug_log_scan(); // Format for display $output = $this->format_debug_log_scan( $results ); wp_send_json_success( array( 'scan_results' => $output, 'raw_data' => $results ) ); } catch ( Exception $e ) { // Enhanced error debugging $debug_info = array(); // Get debug logging status $debug_info['wp_debug'] = defined( 'WP_DEBUG' ) ? ( WP_DEBUG ? 'enabled' : 'disabled' ) : 'not defined'; $debug_info['wp_debug_log'] = defined( 'WP_DEBUG_LOG' ) ? ( WP_DEBUG_LOG ? 'enabled' : 'disabled' ) : 'not defined'; // Get log file path try { $reflection = new ReflectionClass( $scanner ); $method = $reflection->getMethod( 'get_debug_log_path' ); $method->setAccessible( true ); $log_file = $method->invoke( $scanner ); $debug_info['log_file_path'] = $log_file; $debug_info['log_file_exists'] = $log_file ? file_exists( $log_file ) : false; if ( $log_file && file_exists( $log_file ) ) { $debug_info['log_file_size'] = round( filesize( $log_file ) / 1024, 2 ) . ' KB'; $debug_info['log_file_readable'] = is_readable( $log_file ); } } catch ( Exception $path_error ) { $debug_info['log_path_error'] = $path_error->getMessage(); } // Get memory info $debug_info['php_memory_limit'] = ini_get( 'memory_limit' ); $debug_info['current_memory_usage'] = round( memory_get_usage( true ) / 1024 / 1024, 2 ) . ' MB'; wp_send_json_error( array( 'message' => 'Debug log scan failed: ' . $e->getMessage(), 'detailed_message' => $this->format_debug_error_message( $e, $debug_info ), 'error_details' => array( 'file' => $e->getFile(), 'line' => $e->getLine(), 'debug_info' => $debug_info ) ) ); } } /** * Format debug log scan results for display. * * @since 1.8.9.2 * * @param array $results Raw scan results. * @return array */ private function format_debug_log_scan( $results ) { if ( isset( $results['error'] ) ) { return array( 'summary' => "### DEBUG LOG SCAN ERROR ###\n\n" . $results['error']['message'] . "\n", 'technical' => $results['error']['message'], 'forum_ready' => "Debug log scan encountered an error. Please check if WP_DEBUG and WP_DEBUG_LOG are properly configured." ); } if ( ! isset( $results['pre_flight'] ) || ! $results['pre_flight']['passed'] ) { $messages = isset( $results['pre_flight']['messages'] ) ? implode( "\n", $results['pre_flight']['messages'] ) : 'Unknown pre-flight error'; return array( 'summary' => "### DEBUG LOG SCAN UNAVAILABLE ###\n\n" . $messages . "\n", 'technical' => $messages, 'forum_ready' => "Debug log scanning is not available. " . $messages ); } $scanner = Charitable_Tools_Debug_Log_Scanner::get_instance(); $sanitized_errors = $scanner->sanitize_errors_for_sharing( $results['errors'] ); // Generate summary format $summary = "### DEBUG LOG SCAN RESULTS ###\n\n"; $summary .= sprintf( "Generated: %s\n", $results['timestamp'] ?? 'Unknown' ); $summary .= sprintf( "Execution Time: %sms\n", $results['execution_time'] ?? '0' ); $summary .= sprintf( "Scan Timeframe: %d hours\n\n", $results['scan_timeframe'] ?? 24 ); $summary .= "=== SUMMARY ===\n"; $summary .= sprintf( "Total Errors Found: %d\n", $results['statistics']['total_errors'] ); if ( ! empty( $results['statistics']['by_category'] ) ) { $summary .= "\nErrors by Category:\n"; foreach ( $results['statistics']['by_category'] as $category => $count ) { $summary .= sprintf( " %s: %d\n", ucfirst( str_replace( '_', ' ', $category ) ), $count ); } } if ( ! empty( $results['statistics']['by_priority'] ) ) { $summary .= "\nErrors by Priority:\n"; foreach ( $results['statistics']['by_priority'] as $priority => $count ) { $summary .= sprintf( " %s: %d\n", $priority, $count ); } } if ( ! empty( $results['recommendations'] ) ) { $summary .= "\nRecommendations:\n"; foreach ( $results['recommendations'] as $recommendation ) { $summary .= " " . $recommendation . "\n"; } } // Generate technical details $technical = $summary . "\n=== DETAILED ERROR LOG ===\n\n"; if ( ! empty( $sanitized_errors ) ) { foreach ( $sanitized_errors as $error ) { $technical .= sprintf( "[%s] %s - %s\n", $error['datetime'], $error['level'], $error['category'] ); $technical .= sprintf( "Priority: %d (%s)\n", $error['priority'], $error['description'] ); $technical .= sprintf( "Message: %s\n", $error['message'] ); if ( ! empty( $error['file'] ) ) { $technical .= sprintf( "File: %s", $error['file'] ); if ( ! empty( $error['line'] ) ) { $technical .= sprintf( " (line %s)", $error['line'] ); } $technical .= "\n"; } $technical .= "\n" . str_repeat( '-', 50 ) . "\n\n"; } } else { $technical .= "No errors found in the specified timeframe.\n"; } // Generate forum-ready format $forum_ready = "### Charitable Debug Log Scan Results ###\n\n"; $forum_ready .= sprintf( "**WordPress Version:** %s\n", get_bloginfo( 'version' ) ); $forum_ready .= sprintf( "**PHP Version:** %s\n", PHP_VERSION ); $forum_ready .= sprintf( "**Charitable Version:** %s\n\n", charitable()->get_version() ); $forum_ready .= sprintf( "**Scan Summary:** %d errors found in last %d hours\n\n", $results['statistics']['total_errors'], $results['scan_timeframe'] ?? 24 ); if ( ! empty( $results['statistics']['by_category'] ) ) { $forum_ready .= "**Error Categories:**\n"; foreach ( $results['statistics']['by_category'] as $category => $count ) { $forum_ready .= sprintf( "- %s: %d\n", ucfirst( str_replace( '_', ' ', $category ) ), $count ); } $forum_ready .= "\n"; } if ( ! empty( $sanitized_errors ) ) { $forum_ready .= "**Recent Errors (Sanitized):**\n\n"; $error_count = 0; foreach ( $sanitized_errors as $error ) { if ( $error_count >= 5 ) { // Limit to top 5 errors for forum sharing $forum_ready .= sprintf( "... and %d more errors (see technical details)\n\n", count( $sanitized_errors ) - 5 ); break; } $forum_ready .= sprintf( "**Error %d:** [%s] %s\n", $error_count + 1, $error['datetime'], $error['level'] ); $forum_ready .= sprintf( "Category: %s (Priority: %d)\n", $error['category'], $error['priority'] ); $forum_ready .= sprintf( "Message: %s\n", $error['message'] ); if ( ! empty( $error['file'] ) ) { $forum_ready .= sprintf( "File: %s", $error['file'] ); if ( ! empty( $error['line'] ) ) { $forum_ready .= sprintf( " (line %s)", $error['line'] ); } $forum_ready .= "\n"; } $forum_ready .= "\n"; $error_count++; } } if ( ! empty( $results['recommendations'] ) ) { $forum_ready .= "**Recommendations:**\n"; foreach ( $results['recommendations'] as $recommendation ) { $forum_ready .= "- " . $recommendation . "\n"; } } return array( 'summary' => $summary, 'technical' => $technical, 'forum_ready' => $forum_ready ); } /** * Format detailed debug error message. * * @since 1.8.9.2 * * @param Exception $exception The exception that occurred. * @param array $debug_info Debug information array. * @return string */ private function format_debug_error_message( $exception, $debug_info ) { $message = "Debug Log Scanner Error Details:\n\n"; $message .= "Error: " . $exception->getMessage() . "\n"; $message .= "File: " . $exception->getFile() . ":" . $exception->getLine() . "\n\n"; $message .= "WordPress Debug Configuration:\n"; $message .= "- WP_DEBUG: " . $debug_info['wp_debug'] . "\n"; $message .= "- WP_DEBUG_LOG: " . $debug_info['wp_debug_log'] . "\n\n"; if ( isset( $debug_info['log_file_path'] ) ) { $message .= "Log File Information:\n"; $message .= "- Path: " . $debug_info['log_file_path'] . "\n"; $message .= "- Exists: " . ( $debug_info['log_file_exists'] ? 'Yes' : 'No' ) . "\n"; if ( isset( $debug_info['log_file_size'] ) ) { $message .= "- Size: " . $debug_info['log_file_size'] . "\n"; $message .= "- Readable: " . ( $debug_info['log_file_readable'] ? 'Yes' : 'No' ) . "\n"; } $message .= "\n"; } $message .= "System Information:\n"; $message .= "- PHP Memory Limit: " . $debug_info['php_memory_limit'] . "\n"; $message .= "- Current Memory Usage: " . $debug_info['current_memory_usage'] . "\n\n"; // Provide helpful suggestions $message .= "Troubleshooting Suggestions:\n"; if ( $debug_info['wp_debug'] !== 'enabled' || $debug_info['wp_debug_log'] !== 'enabled' ) { $message .= "1. Enable debug logging in wp-config.php:\n"; $message .= " define('WP_DEBUG', true);\n"; $message .= " define('WP_DEBUG_LOG', true);\n\n"; } if ( isset( $debug_info['log_file_exists'] ) && ! $debug_info['log_file_exists'] ) { $message .= "2. No debug.log file found - this is normal if no errors have occurred.\n"; $message .= " Try creating a test error or reproduce an issue first.\n\n"; } if ( isset( $debug_info['log_file_readable'] ) && ! $debug_info['log_file_readable'] ) { $message .= "3. Debug log file is not readable - check file permissions.\n\n"; } $message .= "4. If the issue persists, copy this detailed error message when reporting the issue."; return $message; } /** * Returns and/or create the single instance of this class. * * @since 1.8.1.6 * * @return Charitable_Tools_Settings */ public static function get_instance() { if ( is_null( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } /** * Hosting Environment, Filesystem, and recent Plugin Install error details. * * Designed to surface the signals that matter most when troubleshooting * plugin upload/install failures on managed hosts (WordPress.com, WP * Engine, Kinsta, etc.) where standard PHP limits are misleading. * * @since 1.8.10.5 * * @return string */ private function hosting_environment_info() { $data = "\n" . '-- Hosting Environment' . "\n\n"; $data .= 'Detected Host: ' . self::detect_host() . "\n"; $data .= 'Reverse Proxy/CDN: ' . self::detect_proxy() . "\n"; $data .= "\n" . '-- Filesystem & Updates' . "\n\n"; $data .= 'DISALLOW_FILE_EDIT: ' . self::format_constant_state( 'DISALLOW_FILE_EDIT' ) . "\n"; $data .= 'DISALLOW_FILE_MODS: ' . self::format_constant_state( 'DISALLOW_FILE_MODS' ) . "\n"; $data .= 'AUTOMATIC_UPDATER_DISABLED: ' . self::format_constant_state( 'AUTOMATIC_UPDATER_DISABLED' ) . "\n"; $data .= 'WP_AUTO_UPDATE_CORE: ' . self::format_constant_state( 'WP_AUTO_UPDATE_CORE' ) . "\n"; $data .= 'FS_METHOD: ' . ( defined( 'FS_METHOD' ) ? esc_html( (string) FS_METHOD ) : '(auto-detect)' ) . "\n"; $data .= 'Plugin Dir Writable: ' . ( is_writable( WP_PLUGIN_DIR ) ? 'Yes' : 'No' ) . ' (' . WP_PLUGIN_DIR . ')' . "\n"; $data .= 'wp-content Writable: ' . ( is_writable( WP_CONTENT_DIR ) ? 'Yes' : 'No' ) . "\n"; // Note about misleading PHP limits on managed hosts. if ( self::is_known_managed_host() ) { $data .= "\n" . 'Note: PHP upload/memory limits shown above reflect OS-level config.' . "\n"; $data .= 'Managed hosts may enforce stricter platform-level caps that override them.' . "\n"; } // Recent install errors (captured by the upgrader_install_package_result filter). $errors = (array) get_option( 'charitable_recent_install_errors', array() ); if ( ! empty( $errors ) ) { $data .= "\n" . '-- Recent Plugin Install/Update Errors (last 5)' . "\n\n"; foreach ( array_reverse( $errors ) as $err ) { $ts = isset( $err['time'] ) ? gmdate( 'Y-m-d H:i:s', (int) $err['time'] ) . ' UTC' : '?'; $action = isset( $err['action'] ) ? $err['action'] : '?'; $plugin = isset( $err['plugin'] ) && $err['plugin'] ? $err['plugin'] : '(unknown)'; $code = isset( $err['code'] ) ? $err['code'] : '?'; $msg = isset( $err['message'] ) ? $err['message'] : ''; $data .= '[' . $ts . '] ' . $action . ' ' . $plugin . "\n"; $data .= ' Code: ' . $code . "\n"; $data .= ' Message: ' . wp_strip_all_tags( $msg ) . "\n"; } } return $data; } /** * Detect the hosting platform. * * Read-only: only checks defined constants, loaded classes, and * already-installed mu-plugin filenames. No filesystem scanning beyond * the existing get_mu_plugins() result, no network calls. * * @since 1.8.10.5 * * @return string Human-readable host description, or "Unknown / generic". */ private static function detect_host() { // Cache the result — this is called at least twice per render // (once directly, once via is_known_managed_host()). static $cached = null; if ( null !== $cached ) { return $cached; } // WordPress.com (Atomic infrastructure). $is_wpcom = defined( 'WPCOMSH_VERSION' ) || defined( 'IS_ATOMIC' ) || defined( 'IS_WPCOM' ); if ( ! $is_wpcom && function_exists( 'get_mu_plugins' ) ) { $mu = get_mu_plugins(); if ( isset( $mu['wpcomsh-loader.php'] ) ) { $is_wpcom = true; } } if ( $is_wpcom ) { $cached = 'WordPress.com (' . self::detect_wpcom_plan() . ')'; } elseif ( defined( 'WPE_APIKEY' ) || defined( 'IS_WPE' ) || defined( 'WPE_ATLAS_PLUGIN_VERSION' ) ) { $cached = 'WP Engine'; } elseif ( defined( 'KINSTAMU_VERSION' ) || defined( 'KINSTA_CACHE_ZONE' ) || class_exists( 'Kinsta\\Cache' ) ) { $cached = 'Kinsta'; } elseif ( defined( 'PANTHEON_ENVIRONMENT' ) || isset( $_SERVER['PANTHEON_ENVIRONMENT'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated $cached = 'Pantheon'; } elseif ( defined( 'IS_PRESSABLE' ) || defined( 'PRESSABLE_VERSION' ) ) { $cached = 'Pressable'; } elseif ( defined( 'CLOUDWAYS_VERSION' ) || isset( $_SERVER['cw_allowed_ip'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated $cached = 'Cloudways'; } elseif ( defined( 'GD_SYSTEM_PLUGIN_DIR' ) || class_exists( 'WPaaS\\Plugin' ) ) { $cached = 'GoDaddy Managed WordPress'; } elseif ( defined( 'MM_BASE_DIR' ) || class_exists( 'EPC_Loader' ) ) { $cached = 'Bluehost / Endurance'; } elseif ( defined( 'DH_LITESPEED_PLUGIN' ) || isset( $_SERVER['DH_USER'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated $cached = 'DreamHost (DreamPress)'; } elseif ( class_exists( 'SiteGround_Optimizer\\Loader' ) || class_exists( 'SG_CachePress\\Loader' ) ) { $cached = 'SiteGround'; } elseif ( defined( 'FLYWHEEL_CONFIG_DIR' ) || defined( 'FLYWHEEL_PLUGIN_DIR' ) ) { $cached = 'Flywheel'; } elseif ( defined( 'HOSTINGER_LOCAL_BIN' ) || defined( 'HOSTINGER_AMP_INSTALLED' ) ) { $cached = 'Hostinger'; } else { $cached = 'Unknown / generic'; } return $cached; } /** * Detect WordPress.com plan tier where possible. * * Tries several known surfaces in order; returns "plan unknown" if none * are accessible (WP.com hides this info from most plugin contexts). * * @since 1.8.10.5 * * @return string */ private static function detect_wpcom_plan() { // Direct constant (rare, but possible). if ( defined( 'WPCOMSH_PLAN' ) && constant( 'WPCOMSH_PLAN' ) ) { return (string) constant( 'WPCOMSH_PLAN' ) . ' plan'; } // Jetpack Plan API: present on WP.com sites because Jetpack is bundled. // Wrapped in try/catch because Jetpack_Plan::get() has historically // thrown on partially-initialised Jetpack states, and System Info // must never fatal. if ( class_exists( 'Jetpack_Plan' ) && method_exists( 'Jetpack_Plan', 'get' ) ) { try { $plan = Jetpack_Plan::get(); if ( is_array( $plan ) ) { if ( ! empty( $plan['product_name_short'] ) ) { return $plan['product_name_short'] . ' plan'; } if ( ! empty( $plan['product_slug'] ) ) { return $plan['product_slug'] . ' plan'; } } } catch ( \Throwable $e ) { // phpcs:ignore // Fall through to "plan unknown" on any Jetpack internal error. unset( $e ); } } return 'plan unknown'; } /** * Detect a reverse proxy or CDN in front of the site. * * @since 1.8.10.5 * * @return string */ private static function detect_proxy() { $signals = array(); // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated if ( ! empty( $_SERVER['HTTP_CF_RAY'] ) || ! empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) { $signals[] = 'Cloudflare'; } if ( ! empty( $_SERVER['HTTP_X_SUCURI_CLIENTIP'] ) ) { $signals[] = 'Sucuri'; } if ( ! empty( $_SERVER['HTTP_X_FORWARDED_HOST'] ) && is_string( $_SERVER['HTTP_X_FORWARDED_HOST'] ) ) { $fwd_host = wp_unslash( $_SERVER['HTTP_X_FORWARDED_HOST'] ); if ( is_string( $fwd_host ) && false !== stripos( $fwd_host, 'cdn' ) ) { $signals[] = 'CDN (X-Forwarded-Host)'; } } if ( ! empty( $_SERVER['HTTP_FASTLY_CLIENT_IP'] ) ) { $signals[] = 'Fastly'; } if ( ! empty( $_SERVER['HTTP_X_AKAMAI_EDGESCAPE'] ) ) { $signals[] = 'Akamai'; } // phpcs:enable return empty( $signals ) ? 'None detected' : implode( ', ', $signals ); } /** * Whether the current host is one we know enforces platform-level limits. * * @since 1.8.10.5 * * @return bool */ private static function is_known_managed_host() { $host = self::detect_host(); $managed = array( 'WordPress.com', 'WP Engine', 'Kinsta', 'Pantheon', 'Pressable', 'GoDaddy Managed WordPress', 'Bluehost / Endurance', 'DreamHost (DreamPress)', 'SiteGround', 'Flywheel', ); foreach ( $managed as $needle ) { if ( false !== strpos( $host, $needle ) ) { return true; } } return false; } /** * Render a "constant defined and truthy" report line in a single string. * * @since 1.8.10.5 * * @param string $name Constant name. * @return string */ private static function format_constant_state( $name ) { if ( ! defined( $name ) ) { return 'Not defined'; } $value = constant( $name ); if ( is_bool( $value ) ) { return 'Defined (' . ( $value ? 'true' : 'false' ) . ')'; } if ( is_scalar( $value ) ) { return 'Defined (' . (string) $value . ')'; } return 'Defined'; } /** * Capture failed plugin installs/updates into a small ring buffer. * * Hooked on `upgrader_install_package_result`. Pure pass-through on * success (the filter must return its first argument unchanged in * all branches). Only records WP_Error results for plugin-type * operations. Caps storage at 5 most recent errors. * * @since 1.8.10.5 * * @param array|WP_Error $result The install package result. * @param array $hook_extra Extra args from the upgrader. * @return array|WP_Error Unchanged result. */ public static function log_install_result( $result, $hook_extra = array() ) { // Pass-through: only act on plugin install/update errors. if ( ! is_wp_error( $result ) ) { return $result; } if ( ! is_array( $hook_extra ) || empty( $hook_extra['type'] ) || 'plugin' !== $hook_extra['type'] ) { return $result; } $entry = array( 'time' => time(), 'action' => isset( $hook_extra['action'] ) ? (string) $hook_extra['action'] : '', 'plugin' => isset( $hook_extra['plugin'] ) ? (string) $hook_extra['plugin'] : '', 'code' => $result->get_error_code(), 'message' => $result->get_error_message(), ); $errors = (array) get_option( 'charitable_recent_install_errors', array() ); $errors[] = $entry; $errors = array_slice( $errors, -5 ); update_option( 'charitable_recent_install_errors', $errors, false ); return $result; } } endif;