PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.12.3
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.12.3
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / includes / admin / tools / class-charitable-tools-system-info.php

class-charitable-tools-system-info.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.12.3, at includes/admin/tools/class-charitable-tools-system-info.php

1,982 lines 70.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Tools - System Info.
4 *
5 * @package Charitable/Classes/Charitable_Tools_System_Info
6 * @author David Bisset
7 * @copyright Copyright (c) 2023, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.8.1.6
10 */
11
12 // Exit if accessed directly.
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 if ( ! class_exists( 'Charitable_Tools_System_Info' ) ) :
18
19 /**
20 * Charitable_Tools
21 *
22 * @final
23 * @since 1.8.1.6
24 */
25 class Charitable_Tools_System_Info {
26
27 /**
28 * The single instance of this class.
29 *
30 * @since 1.8.1.6
31 *
32 * @var Charitable_Tools_System_Info|null
33 */
34 private static $instance = null;
35
36 /**
37 * Load the class.
38 *
39 * @since 1.8.1.6
40 */
41 public function load() {
42 // Setup completed - hooks are registered via charitable-tools-admin-hooks.php
43 }
44
45 /**
46 * Hooks.
47 *
48 * @since 1.8.1.6
49 */
50 private function hooks() {
51 // AJAX actions are now registered in charitable-tools-admin-hooks.php
52 }
53
54 /**
55 * Enqueue assets.
56 *
57 * @since 1.8.1.6
58 */
59 public function enqueue_scripts() {
60
61 $min = charitable_get_min_suffix();
62 $version = charitable()->get_version();
63 $assets_dir = charitable()->get_path( 'assets', false );
64
65 /* The following styles are only loaded on Charitable screens. */
66 $screen = get_current_screen();
67
68 if ( ( ! empty( $_GET['page'] ) && 'charitable-tools' === $_GET['page'] ) || ( ! is_null( $screen ) && 'charitable_page_charitable-tools' === $screen->base ) || ( ! empty( $_GET['tab'] ) && 'system-info' === $_GET['tab'] ) ||
69 ( isset( $_SERVER['REQUEST_URI'] ) && strpos( $_SERVER['REQUEST_URI'], 'charitable-tools' ) !== false ) ) { // phpcs:ignore
70
71 wp_enqueue_style(
72 'charitable-system-info',
73 $assets_dir . 'css/tools/system-info' . $min . '.css',
74 array(),
75 $version
76 );
77
78 wp_enqueue_script(
79 'charitable-system-info',
80 $assets_dir . "js/tools/system-info{$min}.js",
81 [ 'jquery' ],
82 $version,
83 true
84 );
85
86 // Localize script for AJAX
87 wp_localize_script(
88 'charitable-system-info',
89 'charitable_email_diagnostics',
90 array(
91 'ajax_url' => admin_url( 'admin-ajax.php' ),
92 'nonce' => wp_create_nonce( 'charitable_email_diagnostics' ),
93 'test_email_nonce' => wp_create_nonce( 'charitable_send_test_email' ),
94 'running_text' => __( 'Running Diagnostics...', 'charitable' ),
95 'error_text' => __( 'Diagnostics failed. Please try again.', 'charitable' ),
96 'sending_text' => __( 'Sending...', 'charitable' )
97 )
98 );
99
100 // Localize script for admin functions (error logs, SSL, etc.)
101 // Use unique variable 'charitable_admin_tools' so it is not overwritten by
102 // charitable-admin-2.0.js which also localizes 'charitable_admin' (no nonce).
103 // Nonce: wp_create_nonce( 'charitable-admin-tools' ), verified by check_ajax_referer( 'charitable-admin-tools', 'nonce', false ).
104 wp_localize_script(
105 'charitable-system-info',
106 'charitable_admin_tools',
107 array(
108 'ajax_url' => admin_url( 'admin-ajax.php' ),
109 'nonce' => wp_create_nonce( 'charitable-admin-tools' ),
110 'testing' => __( 'Testing...', 'charitable' ),
111 )
112 );
113
114 }
115 }
116
117 /**
118 * Get view label.
119 *
120 * @since 1.8.1.6
121 *
122 * @return string
123 */
124 public function get_label() {
125
126 return esc_html__( 'System Info', 'charitable' );
127 }
128
129 /**
130 * Checking user capability to view.
131 *
132 * @since 1.8.1.6
133 *
134 * @return bool
135 */
136 public function check_capability() {
137
138 return charitable_current_user_can();
139 }
140
141 /**
142 * System view content.
143 *
144 * @since 1.8.1.6
145 */
146 public function display() {
147 ?>
148
149 <div class="charitable-system-info-row">
150 <h4 id="system-information"><?php esc_html_e( 'System Information', 'charitable' ); ?></h4>
151 <textarea id="charitable-system-information" class="info-area" readonly><?php echo esc_textarea( $this->get_system_info() ); ?></textarea>
152 <button type="button" id="charitable-system-information-copy" class="button button-primary">
153 <?php esc_html_e( 'Copy System Information', 'charitable' ); ?>
154 </button>
155 </div>
156
157 <div class="charitable-system-info-row">
158 <h4 id="email-diagnostics"><?php esc_html_e( 'Email System Diagnostics', 'charitable' ); ?></h4>
159 <p class="desc"><?php esc_html_e( 'Run comprehensive email system diagnostics to troubleshoot any potential issues.', 'charitable' ); ?></p>
160 <button type="button" id="charitable-email-diagnostics" class="button button-primary">
161 <?php esc_html_e( 'Run Detailed Email Diagnostics', 'charitable' ); ?>
162 </button>
163 <button type="button" id="charitable-send-test-email" class="button button-secondary" style="margin-left: 10px;">
164 <?php esc_html_e( 'Send Test Email', 'charitable' ); ?>
165 </button>
166 <div id="charitable-email-diagnostics-results" style="display:none;">
167 <textarea class="info-area" readonly placeholder="<?php esc_attr_e( 'Diagnostic results will appear here...', 'charitable' ); ?>"></textarea>
168 <button type="button" id="charitable-email-diagnostics-copy" class="button button-secondary" style="display:none;">
169 <?php esc_html_e( 'Copy Diagnostic Results', 'charitable' ); ?>
170 </button>
171 </div>
172 <div id="charitable-test-email-message" style="display:none; margin-top: 10px;">
173 <div class="charitable-notice" style="padding: 10px; border-left: 4px solid #0073aa; background: #f9f9f9;">
174 <p id="charitable-test-email-message-text"></p>
175 </div>
176 </div>
177 </div>
178
179 <div class="charitable-system-info-row compact charitable-settings-row-debug-log-scanner">
180 <h4 id="debug-log-scanner"><?php esc_html_e( 'Debug Log Scanner', 'charitable' ); ?></h4>
181 <p class="desc"><?php esc_html_e( 'Scans WordPress debug logs to help identify issues for troubleshooting.', 'charitable' ); ?></p>
182 <?php
183 $scanner = Charitable_Tools_Debug_Log_Scanner::get_instance();
184 $ui_state = $scanner->get_ui_state();
185
186 // Show status message based on UI state
187 switch ( $ui_state['status'] ) {
188 case 'disabled_no_file':
189 echo '<div class="charitable-notice" style="padding: 10px; border-left: 4px solid #ffba00; background: #fff8e1; margin: 10px 0;">';
190 echo '<p><strong>' . esc_html__( 'WordPress Debug Logging Not Enabled', 'charitable' ) . '</strong></p>';
191 echo '<p>' . 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' ) . '</p>';
192 echo '<p><code style="background: #f0f0f0; padding: 2px 5px; font-family: monospace;">define(\'WP_DEBUG\', true);<br>define(\'WP_DEBUG_LOG\', true);</code></p>';
193 echo '<p>' . sprintf(
194 __( 'For more information see our <a href="%s" target="_blank">guide to debugging WordPress</a>', 'charitable' ),
195 'https://wpcharitable.com/documentation/debugging-wordpress'
196 ) . '</p>';
197 echo '</div>';
198 break;
199
200 case 'no_errors_yet':
201 echo '<div class="charitable-notice" style="padding: 10px; border-left: 4px solid #0073aa; background: #e7f3ff; margin: 10px 0;">';
202 echo '<p><strong>' . esc_html__( 'No Error Log Found', 'charitable' ) . '</strong></p>';
203 echo '<p>' . esc_html( $ui_state['message'] ) . '</p>';
204 echo '</div>';
205 break;
206
207 case 'disabled_but_file_exists':
208 echo '<div class="charitable-notice" style="padding: 10px; border-left: 4px solid #ffba00; background: #fff8e1; margin: 10px 0;">';
209 echo '<p><strong>' . esc_html__( 'WordPress Debug Logging Disabled', 'charitable' ) . '</strong></p>';
210 echo '<p>' . esc_html( $ui_state['message'] ) . '</p>';
211 echo '<p>' . sprintf(
212 __( 'To enable debug logging see our <a href="%s" target="_blank">guide to debugging WordPress</a>', 'charitable' ),
213 'https://wpcharitable.com/documentation/debugging-wordpress'
214 ) . '</p>';
215 echo '</div>';
216 break;
217 }
218 ?>
219
220 <?php if ( $ui_state['show_button'] ) : ?>
221 <button type="button" id="charitable-debug-log-scan" class="button button-primary" <?php echo $ui_state['button_enabled'] ? '' : 'disabled'; ?>>
222 <?php esc_html_e( 'Scan Debug Log', 'charitable' ); ?>
223 </button>
224 <p style="margin-top: 5px; color: #666; font-size: 13px;">
225 <?php
226 if ( $ui_state['status'] === 'disabled_but_file_exists' ) {
227 esc_html_e( 'Scanning historical log data. Scans the last 24 hours by default.', 'charitable' );
228 } else {
229 esc_html_e( 'Scans the last 24 hours by default.', 'charitable' );
230 }
231 ?>
232 </p>
233 <?php endif; ?>
234 <div id="charitable-debug-log-results" style="display:none; margin-top: 15px;">
235 <div class="charitable-debug-format-tabs" style="margin-bottom: 10px;">
236 <button type="button" class="button button-secondary charitable-format-tab active" data-format="summary">
237 <?php esc_html_e( 'Summary', 'charitable' ); ?>
238 </button>
239 <button type="button" class="button button-secondary charitable-format-tab" data-format="technical">
240 <?php esc_html_e( 'Technical Details', 'charitable' ); ?>
241 </button>
242 <button type="button" class="button button-secondary charitable-format-tab" data-format="forum">
243 <?php esc_html_e( 'Forum-Ready', 'charitable' ); ?>
244 </button>
245 </div>
246 <textarea id="charitable-debug-summary" class="info-area charitable-format-content" readonly placeholder="<?php esc_attr_e( 'Debug log scan summary will appear here...', 'charitable' ); ?>"></textarea>
247 <textarea id="charitable-debug-technical" class="info-area charitable-format-content" readonly placeholder="<?php esc_attr_e( 'Technical details will appear here...', 'charitable' ); ?>" style="display:none;"></textarea>
248 <textarea id="charitable-debug-forum" class="info-area charitable-format-content" readonly placeholder="<?php esc_attr_e( 'Forum-ready format will appear here...', 'charitable' ); ?>" style="display:none;"></textarea>
249 <div style="margin-top: 10px;">
250 <button type="button" id="charitable-debug-copy" class="button button-secondary">
251 <?php esc_html_e( 'Copy Current View', 'charitable' ); ?>
252 </button>
253 <span id="charitable-debug-copy-message" style="margin-left: 10px; color: #46b450; display: none;">
254 <?php esc_html_e( 'Copied to clipboard!', 'charitable' ); ?>
255 </span>
256 </div>
257 </div>
258 <div id="charitable-debug-log-message" style="display:none; margin-top: 10px;">
259 <div class="charitable-notice" style="padding: 10px; border-left: 4px solid #0073aa; background: #f9f9f9;">
260 <p id="charitable-debug-log-message-text"></p>
261 </div>
262 </div>
263 </div>
264
265 <div class="charitable-system-info-row compact charitable-settings-row-error-logs">
266 <h4 id="donation-error-logs"><?php esc_html_e( 'Donation Form Error Logs', 'charitable' ); ?></h4>
267 <p class="desc"><?php esc_html_e( 'Recent donation form errors and troubleshooting information.', 'charitable' ); ?></p>
268 <?php
269 $recent_errors = charitable_get_recent_form_errors( 10 );
270 if ( ! empty( $recent_errors ) ) {
271 $error_count = count( charitable_get_recent_form_errors( 50 ) );
272 echo '<p><strong>' . sprintf( esc_html( _n( '%d error found in recent logs', '%d errors found in recent logs', $error_count, 'charitable' ) ), $error_count ) . '</strong></p>';
273 ?>
274 <p>
275 <button type="button" id="charitable-toggle-error-logs" class="button button-secondary">
276 <?php esc_html_e( 'Show Error Logs', 'charitable' ); ?>
277 </button>
278 </p>
279 <div id="charitable-error-logs-container" style="display: none; margin-top: 15px;">
280 <div style="max-height: 300px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; background: #f9f9f9;">
281 <?php foreach ( array_slice( $recent_errors, 0, 10 ) as $error ) : ?>
282 <div style="margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee;">
283 <strong><?php echo esc_html( $error->date_recorded ); ?></strong>
284 <span style="color: #d63638;"><?php echo esc_html( ucwords( str_replace( '_', ' ', $error->primary_action ) ) ); ?></span>
285 - <?php echo esc_html( ucwords( str_replace( '_', ' ', $error->secondary_action ) ) ); ?>
286 <?php if ( $error->campaign_id ) : ?>
287 <br><small>Campaign: <?php echo esc_html( get_the_title( $error->campaign_id ) ); ?> (#<?php echo intval( $error->campaign_id ); ?>)</small>
288 <?php endif; ?>
289 <?php if ( $error->amount ) : ?>
290 <small>| Amount: <?php echo esc_html( charitable_format_money( $error->amount ) ); ?></small>
291 <?php endif; ?>
292 <?php if ( ! empty( $error->meta_data['error_message'] ) ) : ?>
293 <br><small style="color: #826200;">→ <?php echo esc_html( $error->meta_data['error_message'] ); ?></small>
294 <?php endif; ?>
295 <?php if ( ! empty( $error->meta_data['email_class'] ) ) : ?>
296 <small style="color: #888;">[<?php echo esc_html( $error->meta_data['email_class'] ); ?>]</small>
297 <?php endif; ?>
298 </div>
299 <?php endforeach; ?>
300 </div>
301 <p style="margin-top: 10px;">
302 <button type="button" id="charitable-clear-error-logs" class="button button-secondary">
303 <?php esc_html_e( 'Clear Error Logs', 'charitable' ); ?>
304 </button>
305 <button type="button" id="charitable-export-error-logs" class="button button-secondary" style="margin-left: 10px;">
306 <?php esc_html_e( 'Export Error Logs', 'charitable' ); ?>
307 </button>
308 </p>
309 </div>
310 <?php
311 } else {
312 echo '<p style="color: #46b450;"><strong>' . esc_html__( 'No recent donation form errors found.', 'charitable' ) . '</strong></p>';
313 }
314 ?>
315 <div id="charitable-error-log-message" style="display:none; margin-top: 10px;">
316 <div class="charitable-notice" style="padding: 10px; border-left: 4px solid #0073aa; background: #f9f9f9;">
317 <p id="charitable-error-log-message-text"></p>
318 </div>
319 </div>
320 </div>
321
322 <div class="charitable-system-info-row last charitable-settings-row-test-ssl">
323 <h4 id="ssl-verify"><?php esc_html_e( 'Test SSL Connections', 'charitable' ); ?></h4>
324 <p class="desc"><?php esc_html_e( 'Click the button below to verify your web server can perform SSL connections successfully.', 'charitable' ); ?></p>
325 <button type="button" id="charitable-ssl-verify" class="button button-primary">
326 <?php esc_html_e( 'Test Connection', 'charitable' ); ?>
327 </button>
328 </div>
329
330 <?php
331 }
332
333 /**
334 * Get system information.
335 *
336 * Based on a function from Easy Digital Downloads by Pippin Williamson.
337 *
338 * @link https://github.com/easydigitaldownloads/easy-digital-downloads/blob/master/includes/admin/tools.php#L470
339 *
340 * @since 1.8.1.6
341 *
342 * @return string
343 */
344 public function get_system_info() {
345
346 $data = '### Begin System Info ###' . "\n\n";
347
348 $data .= $this->charitable_info();
349 $data .= $this->site_info();
350 $data .= $this->wp_info();
351 $data .= $this->uploads_info();
352 $data .= $this->plugins_info();
353 $data .= $this->server_info();
354 $data .= $this->charitable_privacy_settings_info();
355 $data .= $this->email_diagnostics();
356 $data .= $this->donation_error_logs();
357 $data .= $this->debug_log_scanner();
358 $data .= $this->hosting_environment_info();
359
360 $data .= "\n" . '### End System Info ###';
361
362 return $data;
363 }
364
365 /**
366 * Get Charitable info.
367 *
368 * @since 1.8.1.6
369 * @version 1.8.3 added version upgraded from.
370 * @version 1.8.4.5
371 *
372 * @return string
373 */
374 private function charitable_info() {
375
376 $data = '-- Charitable Info' . "\n\n";
377
378 $activated = get_option( 'wpcharitable_activated_datetime', false );
379
380 $slug_dismissed = get_user_meta( get_current_user_id(), 'charitable-pointer-slug-dismissed', true );
381 $dismissed_addons = get_user_meta( get_current_user_id(), 'charitable_dismissed_addons', true );
382
383 $wpc_plugin_version = (array) get_site_option( 'wpc_plugin_version' );
384
385 $wpc_plugin_failure = (array) get_option( 'wpc_edd_sl_failed_plugin_versions' );
386
387 $versions_upgraded_from = get_option( 'charitable_version_upgraded_from', [] );
388 if ( ! empty( $versions_upgraded_from ) ) {
389 $versions_upgraded_from = array_unique( $versions_upgraded_from );
390 }
391
392 if ( ! empty( $versions_upgraded_from ) ) {
393 $data .= 'Versions Upgraded From: ' . implode( ',', $versions_upgraded_from ) . "\n";
394 }
395 if ( ! empty( $slug_dismissed ) ) {
396 $data .= 'Pointer Slugs Dismissed: ' . implode( ',', $slug_dismissed ) . "\n";
397 }
398 if ( ! empty( $dismissed_addons ) ) {
399 $data .= 'G Removals: ' . implode( ',', $dismissed_addons ) . "\n";
400 }
401 if ( ! empty( array_filter( $wpc_plugin_version ) ) ) {
402 $data .= 'Plugin Version DEBUG 1: ' . implode( ',', $wpc_plugin_version ) . "\n";
403 }
404 if ( ! empty( array_filter( $wpc_plugin_failure ) ) ) {
405 $data .= 'Plugin Version DEBUG 2: ' . implode( ',', $wpc_plugin_failure ) . "\n";
406 }
407 if ( $activated ) {
408 $data .= 'Activated: ' . $this->get_formatted_datetime( $activated ) . "\n";
409 }
410 return $data;
411 }
412
413 /**
414 * Get Charitable privacy settings (for copyable system information).
415 *
416 * @since 1.8.1.6
417 *
418 * @return string
419 */
420 private function charitable_privacy_settings_info() {
421
422 if ( ! function_exists( 'charitable_get_option' ) ) {
423 return '';
424 }
425
426 $retention_labels = array(
427 0 => __( 'None', 'charitable' ),
428 1 => __( 'One year', 'charitable' ),
429 2 => __( 'Two years', 'charitable' ),
430 3 => __( 'Three years', 'charitable' ),
431 4 => __( 'Four years', 'charitable' ),
432 5 => __( 'Five years', 'charitable' ),
433 6 => __( 'Six years', 'charitable' ),
434 7 => __( 'Seven years', 'charitable' ),
435 8 => __( 'Eight years', 'charitable' ),
436 9 => __( 'Nine years', 'charitable' ),
437 10 => __( 'Ten years', 'charitable' ),
438 'endless' => __( 'Forever', 'charitable' ),
439 );
440
441 $retention_period = charitable_get_option( 'minimum_data_retention_period', 2 );
442 $retention_label = isset( $retention_labels[ $retention_period ] ) ? $retention_labels[ $retention_period ] : (string) $retention_period;
443
444 $data = "\n" . '-- Charitable Privacy Settings' . "\n\n";
445 $data .= 'Minimum Data Retention: ' . $retention_label . "\n";
446
447 $retention_fields = charitable_get_option( 'data_retention_fields', array() );
448 if ( is_array( $retention_fields ) && ! empty( $retention_fields ) ) {
449 $keys = array_keys( $retention_fields );
450 $list = array_values( $keys ) === range( 0, count( $keys ) - 1 );
451 $names = $list ? array_values( $retention_fields ) : $keys;
452 $data .= 'Retained Data Fields: ' . implode( ', ', $names ) . "\n";
453 } else {
454 $data .= 'Retained Data Fields: (none or all)' . "\n";
455 }
456
457 $data .= 'Contact Consent Field: ' . ( charitable_get_option( 'contact_consent', 0 ) ? 'Enabled' : 'Disabled' ) . "\n";
458 $data .= 'Consent Required (Forms): ' . ( charitable_get_option( 'contact_consent_required', false ) ? 'Yes' : 'No' ) . "\n";
459
460 $contact_label_default = __( 'Yes, I am happy for you to contact me via email or phone.', 'charitable' );
461 $contact_label_value = charitable_get_option( 'contact_consent_label', $contact_label_default );
462 $data .= 'Contact Consent Label: ' . $this->privacy_text_summary( $contact_label_value, $contact_label_default ) . "\n";
463
464 $data .= 'Privacy Policy Field: ' . ( charitable_get_option( 'privacy_policy_enabled', 0 ) ? 'Enabled' : 'Disabled' ) . "\n";
465
466 $privacy_page_id = charitable_get_option( 'privacy_policy_page', 0 );
467 if ( ! empty( $privacy_page_id ) && get_post_status( $privacy_page_id ) ) {
468 $data .= 'Privacy Policy Page: ' . get_the_title( $privacy_page_id ) . ' (#' . (int) $privacy_page_id . ')' . "\n";
469 } else {
470 $data .= 'Privacy Policy Page: Not set' . "\n";
471 }
472
473 $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' );
474 $privacy_policy_value = charitable_get_option( 'privacy_policy', $privacy_policy_default );
475 $data .= 'Privacy Policy Text: ' . $this->privacy_text_summary( $privacy_policy_value, $privacy_policy_default ) . "\n";
476
477 $data .= 'Terms and Conditions Field: ' . ( charitable_get_option( 'terms_and_conditions_enabled', 0 ) ? 'Enabled' : 'Disabled' ) . "\n";
478
479 $terms_page_id = charitable_get_option( 'terms_conditions_page', 0 );
480 if ( ! empty( $terms_page_id ) && get_post_status( $terms_page_id ) ) {
481 $data .= 'Terms and Conditions Page: ' . get_the_title( $terms_page_id ) . ' (#' . (int) $terms_page_id . ')' . "\n";
482 } else {
483 $data .= 'Terms and Conditions Page: Not set' . "\n";
484 }
485
486 $terms_default = __( 'I have read and agree to the website [terms].', 'charitable' );
487 $terms_value = charitable_get_option( 'terms_conditions', $terms_default );
488 $data .= 'Terms and Conditions Text: ' . $this->privacy_text_summary( $terms_value, $terms_default ) . "\n";
489
490 return $data;
491 }
492
493 /**
494 * Short summary for a privacy text option: "Default" or "Custom (X characters)".
495 *
496 * @since 1.8.1.6
497 *
498 * @param string $value Current option value.
499 * @param string $default Default text to compare against.
500 * @return string
501 */
502 private function privacy_text_summary( $value, $default ) {
503
504 $value = is_string( $value ) ? trim( $value ) : '';
505 $default = is_string( $default ) ? trim( $default ) : '';
506
507 if ( $value === $default || $value === '' ) {
508 return 'Default';
509 }
510
511 return 'Custom (' . strlen( $value ) . ' characters)';
512 }
513
514 /**
515 * Get Site info.
516 *
517 * @since 1.8.1.6
518 *
519 * @return string
520 */
521 private function site_info() {
522
523 $data = "\n" . '-- Site Info' . "\n\n";
524 $data .= 'Site URL: ' . site_url() . "\n";
525 $data .= 'Home URL: ' . home_url() . "\n";
526 $data .= 'Multisite: ' . ( is_multisite() ? 'Yes' : 'No' ) . "\n";
527
528 return $data;
529 }
530
531 /**
532 * Get WordPress Configuration info.
533 *
534 * @since 1.8.1.6
535 *
536 * @return string
537 */
538 private function wp_info() {
539
540 global $wpdb;
541
542 $theme_data = wp_get_theme();
543 $theme = $theme_data->name . ' ' . $theme_data->version;
544
545 $data = "\n" . '-- WordPress Configuration' . "\n\n";
546 $data .= 'Version: ' . get_bloginfo( 'version' ) . "\n";
547 $data .= 'Language: ' . get_locale() . "\n";
548 $data .= 'User Language: ' . get_user_locale() . "\n";
549 $data .= 'Permalink Structure: ' . ( get_option( 'permalink_structure' ) ? get_option( 'permalink_structure' ) : 'Default' ) . "\n";
550 $data .= 'Active Theme: ' . $theme . "\n";
551 $data .= 'Show On Front: ' . get_option( 'show_on_front' ) . "\n";
552
553 // Only show page specs if front page is set to 'page'.
554 if ( get_option( 'show_on_front' ) === 'page' ) {
555 $front_page_id = get_option( 'page_on_front' );
556 $blog_page_id = get_option( 'page_for_posts' );
557
558 $data .= 'Page On Front: ' . ( $front_page_id ? get_the_title( $front_page_id ) . ' (#' . $front_page_id . ')' : 'Unset' ) . "\n";
559 $data .= 'Page For Posts: ' . ( $blog_page_id ? get_the_title( $blog_page_id ) . ' (#' . $blog_page_id . ')' : 'Unset' ) . "\n";
560 }
561 $data .= 'ABSPATH: ' . ABSPATH . "\n";
562 $data .= 'Table Prefix: ' . 'Length: ' . strlen( $wpdb->prefix ) . ' Status: ' . ( strlen( $wpdb->prefix ) > 16 ? 'ERROR: Too long' : 'Acceptable' ) . "\n"; //phpcs:ignore
563 $data .= 'WP_DEBUG: ' . ( defined( 'WP_DEBUG' ) ? WP_DEBUG ? 'Enabled' : 'Disabled' : 'Not set' ) . "\n";
564 $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";
565 $data .= 'WP_DEBUG_DISPLAY: ' . ( defined( 'WP_DEBUG_DISPLAY' ) ? WP_DEBUG_DISPLAY ? 'Enabled' : 'Disabled' : 'Not set' ) . "\n";
566 $data .= 'SCRIPT_DEBUG: ' . ( defined( 'SCRIPT_DEBUG' ) ? SCRIPT_DEBUG ? 'Enabled' : 'Disabled' : 'Not set' ) . "\n";
567 $data .= 'CHARITABLE_DEBUG: ' . ( defined( 'CHARITABLE_DEBUG' ) ? CHARITABLE_DEBUG ? 'Enabled' : 'Disabled' : 'Not set' ) . "\n";
568 $data .= 'CHARITABLE_DEBUG_SECURITY: ' . ( defined( 'CHARITABLE_DEBUG_SECURITY' ) ? CHARITABLE_DEBUG_SECURITY ? 'Enabled' : 'Disabled' : 'Not set' ) . "\n";
569 $data .= 'Memory Limit: ' . WP_MEMORY_LIMIT . "\n";
570 $data .= 'Registered Post Stati: ' . implode( ', ', get_post_stati() ) . "\n";
571 $data .= 'Revisions: ' . ( WP_POST_REVISIONS ? WP_POST_REVISIONS > 1 ? 'Limited to ' . WP_POST_REVISIONS : 'Enabled' : 'Disabled' ) . "\n";
572
573 return $data;
574 }
575
576 /**
577 * Get Uploads/Constants info.
578 *
579 * @since 1.8.1.6
580 *
581 * @return string
582 */
583 private function uploads_info() {
584
585 // @todo Charitable configuration/specific details.
586 $data = "\n" . '-- WordPress Uploads/Constants' . "\n\n";
587 $data .= 'WP_CONTENT_DIR: ' . ( defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR ? WP_CONTENT_DIR : 'Disabled' : 'Not set' ) . "\n";
588 $data .= 'WP_CONTENT_URL: ' . ( defined( 'WP_CONTENT_URL' ) ? WP_CONTENT_URL ? WP_CONTENT_URL : 'Disabled' : 'Not set' ) . "\n";
589 $data .= 'UPLOADS: ' . ( defined( 'UPLOADS' ) ? UPLOADS ? UPLOADS : 'Disabled' : 'Not set' ) . "\n";
590
591 $uploads_dir = wp_upload_dir();
592
593 $data .= 'wp_uploads_dir() path: ' . $uploads_dir['path'] . "\n";
594 $data .= 'wp_uploads_dir() url: ' . $uploads_dir['url'] . "\n";
595 $data .= 'wp_uploads_dir() basedir: ' . $uploads_dir['basedir'] . "\n";
596 $data .= 'wp_uploads_dir() baseurl: ' . $uploads_dir['baseurl'] . "\n";
597
598 return $data;
599 }
600
601 /**
602 * Get Plugins info.
603 *
604 * @since 1.8.1.6
605 *
606 * @return string
607 */
608 private function plugins_info() {
609
610 // Get plugins that have an update.
611 $data = $this->mu_plugins();
612 $data .= $this->installed_plugins();
613 $data .= $this->multisite_plugins();
614
615 return $data;
616 }
617
618 /**
619 * Get MU Plugins info.
620 *
621 * @since 1.8.1.6
622 *
623 * @return string
624 */
625 private function mu_plugins() {
626
627 $data = '';
628
629 // Must-use plugins.
630 // NOTE: MU plugins can't show updates!
631 $muplugins = get_mu_plugins();
632
633 if ( ! empty( $muplugins ) && count( $muplugins ) > 0 ) {
634 $data = "\n" . '-- Must-Use Plugins' . "\n\n";
635
636 foreach ( $muplugins as $plugin => $plugin_data ) {
637 $data .= $plugin_data['Name'] . ': ' . $plugin_data['Version'] . "\n";
638 }
639 }
640
641 return $data;
642 }
643
644 /**
645 * Get Installed Plugins info.
646 *
647 * @since 1.8.1.6
648 *
649 * @return string
650 */
651 private function installed_plugins() {
652
653 $updates = function_exists( 'get_plugin_updates' ) ? get_plugin_updates() : array();
654
655 // WordPress active plugins.
656 $data = "\n" . '-- WordPress Active Plugins' . "\n\n";
657
658 $plugins = get_plugins();
659 $active_plugins = get_option( 'active_plugins', [] );
660
661 foreach ( $plugins as $plugin_path => $plugin ) {
662 if ( ! in_array( $plugin_path, $active_plugins, true ) ) {
663 continue;
664 }
665 $update = ( array_key_exists( $plugin_path, $updates ) ) ? ' (needs update - ' . $updates[ $plugin_path ]->update->new_version . ')' : '';
666 $data .= $plugin['Name'] . ': ' . $plugin['Version'] . $update . "\n";
667 }
668
669 // WordPress inactive plugins.
670 $data .= "\n" . '-- WordPress Inactive Plugins' . "\n\n";
671
672 foreach ( $plugins as $plugin_path => $plugin ) {
673 if ( in_array( $plugin_path, $active_plugins, true ) ) {
674 continue;
675 }
676 $update = ( array_key_exists( $plugin_path, $updates ) ) ? ' (needs update - ' . $updates[ $plugin_path ]->update->new_version . ')' : '';
677 $data .= $plugin['Name'] . ': ' . $plugin['Version'] . $update . "\n";
678 }
679
680 return $data;
681 }
682
683 /**
684 * Get Multisite Plugins info.
685 *
686 * @since 1.8.1.6
687 *
688 * @return string
689 */
690 private function multisite_plugins() {
691
692 $data = '';
693
694 if ( ! is_multisite() ) {
695 return $data;
696 }
697
698 $updates = function_exists( 'get_plugin_updates' ) ? get_plugin_updates() : array();
699
700 // WordPress Multisite active plugins.
701 $data = "\n" . '-- Network Active Plugins' . "\n\n";
702
703 $plugins = wp_get_active_network_plugins();
704 $active_plugins = get_site_option( 'active_sitewide_plugins', [] );
705
706 foreach ( $plugins as $plugin_path ) {
707 $plugin_base = plugin_basename( $plugin_path );
708
709 if ( ! array_key_exists( $plugin_base, $active_plugins ) ) {
710 continue;
711 }
712 $update = ( array_key_exists( $plugin_path, $updates ) ) ? ' (needs update - ' . $updates[ $plugin_path ]->update->new_version . ')' : '';
713 $plugin = get_plugin_data( $plugin_path );
714 $data .= $plugin['Name'] . ': ' . $plugin['Version'] . $update . "\n";
715 }
716
717 return $data;
718 }
719
720 /**
721 * Get Server info.
722 *
723 * @since 1.8.1.6
724 *
725 * @return string
726 */
727 private function server_info() {
728
729 global $wpdb;
730
731 // Server configuration (really just versions).
732 $data = "\n" . '-- Webserver Configuration' . "\n\n";
733 $data .= 'PHP Version: ' . PHP_VERSION . "\n";
734 $data .= 'MySQL Version: ' . $wpdb->db_version() . "\n";
735 $data .= 'Webserver Info: ' . ( isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '' ) . "\n";
736
737 // PHP configs... now we're getting to the important stuff.
738 $data .= "\n" . '-- PHP Configuration' . "\n\n";
739 $data .= 'Memory Limit: ' . ini_get( 'memory_limit' ) . "\n";
740 $data .= 'Upload Max Size: ' . ini_get( 'upload_max_filesize' ) . "\n";
741 $data .= 'Post Max Size: ' . ini_get( 'post_max_size' ) . "\n";
742 $data .= 'Upload Max Filesize: ' . ini_get( 'upload_max_filesize' ) . "\n";
743 $data .= 'Time Limit: ' . ini_get( 'max_execution_time' ) . "\n";
744 $data .= 'Max Input Vars: ' . ini_get( 'max_input_vars' ) . "\n";
745 $data .= 'Display Errors: ' . ( ini_get( 'display_errors' ) ? 'On (' . ini_get( 'display_errors' ) . ')' : 'N/A' ) . "\n";
746
747 // PHP extensions and such.
748 $data .= "\n" . '-- PHP Extensions' . "\n\n";
749 $data .= 'cURL: ' . ( function_exists( 'curl_init' ) ? 'Supported' : 'Not Supported' ) . "\n";
750 $data .= 'fsockopen: ' . ( function_exists( 'fsockopen' ) ? 'Supported' : 'Not Supported' ) . "\n";
751 $data .= 'SOAP Client: ' . ( class_exists( 'SoapClient', false ) ? 'Installed' : 'Not Installed' ) . "\n";
752 $data .= 'Suhosin: ' . ( extension_loaded( 'suhosin' ) ? 'Installed' : 'Not Installed' ) . "\n";
753
754 // Session stuff.
755 $data .= "\n" . '-- Session Configuration' . "\n\n";
756 $data .= 'Session: ' . ( isset( $_SESSION ) ? 'Enabled' : 'Disabled' ) . "\n";
757
758 // The rest of this is only relevant if session is enabled.
759 if ( isset( $_SESSION ) ) {
760 $data .= 'Session Name: ' . esc_html( ini_get( 'session.name' ) ) . "\n";
761 $data .= 'Cookie Path: ' . esc_html( ini_get( 'session.cookie_path' ) ) . "\n";
762 $data .= 'Save Path: ' . esc_html( ini_get( 'session.save_path' ) ) . "\n";
763 $data .= 'Use Cookies: ' . ( ini_get( 'session.use_cookies' ) ? 'On' : 'Off' ) . "\n";
764 $data .= 'Use Only Cookies: ' . ( ini_get( 'session.use_only_cookies' ) ? 'On' : 'Off' ) . "\n";
765 }
766
767 return $data;
768 }
769
770 /**
771 * Get formatted datetime.
772 *
773 * @since 1.8.5
774 *
775 * @param int|string $date Date.
776 *
777 * @return string
778 */
779 private function get_formatted_datetime( $date ) {
780
781 return sprintf(
782 '%1$s at %2$s (GMT)',
783 gmdate( 'M j, Y', $date ),
784 gmdate( 'g:ia', $date )
785 );
786 }
787
788 /**
789 * Get email diagnostics summary for system info.
790 *
791 * @since 1.8.9.2
792 *
793 * @return string
794 */
795 private function email_diagnostics() {
796 try {
797 $diagnostics = Charitable_Tools_Email_Diagnostics::get_instance();
798 return $diagnostics->get_email_diagnostics_summary();
799 } catch ( Exception $e ) {
800 return "\n-- Email System Diagnostics\n\nError: Unable to run email diagnostics - " . $e->getMessage() . "\n";
801 }
802 }
803
804 /**
805 * AJAX handler for detailed email diagnostics.
806 *
807 * @since 1.8.9.2
808 *
809 * @return void
810 */
811 public function ajax_email_diagnostics() {
812 // Check nonce
813 if ( ! wp_verify_nonce( $_POST['nonce'], 'charitable_email_diagnostics' ) ) {
814 wp_die( __( 'Security check failed', 'charitable' ) );
815 }
816
817 // Check capability
818 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
819 wp_die( __( 'Insufficient permissions', 'charitable' ) );
820 }
821
822 try {
823 $diagnostics = Charitable_Tools_Email_Diagnostics::get_instance();
824 $results = $diagnostics->get_detailed_email_diagnostics();
825
826 // Format for display
827 $output = $this->format_detailed_diagnostics( $results );
828
829 wp_send_json_success( array(
830 'diagnostics' => $output,
831 'raw_data' => $results
832 ) );
833
834 } catch ( Exception $e ) {
835 wp_send_json_error( array(
836 'message' => 'Diagnostics failed: ' . $e->getMessage(),
837 'error_details' => array(
838 'file' => $e->getFile(),
839 'line' => $e->getLine()
840 )
841 ) );
842 }
843 }
844
845 /**
846 * Format detailed diagnostics for display.
847 *
848 * @since 1.8.9.2
849 *
850 * @param array $results Raw diagnostic results.
851 * @return string Formatted diagnostic output.
852 */
853 private function format_detailed_diagnostics( $results ) {
854 if ( isset( $results['error'] ) ) {
855 return "### DIAGNOSTIC ERROR ###\n\n" . $results['error']['message'] . "\n";
856 }
857
858 $output = "### DETAILED EMAIL DIAGNOSTICS ###\n\n";
859 $output .= sprintf( "Generated: %s\n", $results['timestamp'] ?? 'Unknown' );
860 $output .= sprintf( "Execution Time: %sms\n\n", $results['execution_time'] ?? '0' );
861
862 // Health Score Section
863 if ( isset( $results['health_score'] ) ) {
864 $health = $results['health_score'];
865 $output .= "=== OVERALL HEALTH SCORE ===\n";
866 $output .= sprintf( "Score: %d/%d (%d%%) - %s\n",
867 $health['total_score'], $health['max_score'], $health['percentage'], $health['grade'] );
868 $output .= sprintf( "Assessment: %s\n\n", $health['interpretation'] );
869
870 // Category breakdown
871 $output .= "Category Scores:\n";
872 foreach ( $health['category_scores'] as $category => $score ) {
873 $output .= sprintf( " %-20s: %2d/%-2d (%3d%%)\n",
874 ucwords( str_replace( '_', ' ', $category ) ),
875 $score['score'], $score['max_score'], $score['percentage'] );
876 }
877 $output .= "\n";
878 }
879
880 // Individual Test Results
881 $test_sections = array(
882 'email_registration' => 'EMAIL REGISTRATION TEST',
883 'email_access' => 'EMAIL ACCESS TEST',
884 'preview_functionality' => 'PREVIEW FUNCTIONALITY TEST',
885 'hook_timing' => 'HOOK TIMING TEST',
886 'environment_config' => 'ENVIRONMENT CONFIGURATION',
887 'performance' => 'PERFORMANCE METRICS'
888 );
889
890 foreach ( $test_sections as $key => $title ) {
891 if ( ! isset( $results[ $key ] ) ) {
892 continue;
893 }
894
895 $test = $results[ $key ];
896 $output .= "=== {$title} ===\n";
897 $output .= sprintf( "Status: %s (Score: %d/%d)\n",
898 strtoupper( $test['status'] ), $test['score'], $test['max_score'] );
899
900 // Test-specific details
901 switch ( $key ) {
902 case 'email_registration':
903 $this->format_registration_details( $test, $output );
904 break;
905 case 'email_access':
906 $this->format_access_details( $test, $output );
907 break;
908 case 'preview_functionality':
909 $this->format_preview_details( $test, $output );
910 break;
911 case 'hook_timing':
912 $this->format_timing_details( $test, $output );
913 break;
914 case 'environment_config':
915 $this->format_environment_details( $test, $output );
916 break;
917 case 'performance':
918 $this->format_performance_details( $test, $output );
919 break;
920 }
921
922 $output .= "\n";
923 }
924
925 $output .= "### END DETAILED DIAGNOSTICS ###";
926 return $output;
927 }
928
929 /**
930 * Format registration test details.
931 *
932 * @since 1.8.9.2
933 *
934 * @param array $test Test results.
935 * @param string &$output Output string to append to.
936 */
937 private function format_registration_details( $test, &$output ) {
938 $details = $test['details'];
939
940 $output .= sprintf( "Emails Registered: %d\n", count( $test['registered_emails'] ?? array() ) );
941 $output .= sprintf( "Core Emails Present: %d/%d\n",
942 $details['core_emails_present'] ?? 0, $details['core_emails_total'] ?? 3 );
943 $output .= sprintf( "Registration Time: %sms\n",
944 $test['timing']['duration_ms'] ?? '0' );
945
946 if ( isset( $details['helper_accessible'] ) && ! $details['helper_accessible'] ) {
947 $output .= "⚠ Helper Access: FAILED\n";
948 if ( isset( $details['helper_error'] ) ) {
949 $output .= " Error: " . $details['helper_error'] . "\n";
950 }
951 }
952
953 if ( ! empty( $test['registered_emails'] ) ) {
954 $output .= "Registered Email Classes:\n";
955 foreach ( $test['registered_emails'] as $id => $class ) {
956 $output .= " {$id}: {$class}\n";
957 }
958 }
959 }
960
961 /**
962 * Format access test details.
963 *
964 * @since 1.8.9.2
965 *
966 * @param array $test Test results.
967 * @param string &$output Output string to append to.
968 */
969 private function format_access_details( $test, &$output ) {
970 $details = $test['details'];
971
972 $output .= sprintf( "Successful Access: %d/%d\n",
973 $details['successful_accesses'] ?? 0, $details['total_emails_tested'] ?? 3 );
974 $output .= sprintf( "Average Access Time: %.1fms\n", $test['average_time'] ?? 0 );
975
976 if ( isset( $test['email_tests'] ) ) {
977 $output .= "Individual Email Tests:\n";
978 foreach ( $test['email_tests'] as $email_id => $email_test ) {
979 $status = $email_test['accessible'] ? '✓' : '✗';
980 $output .= sprintf( " %s %-25s: %s (%.1fms)\n",
981 $status, $email_id,
982 $email_test['accessible'] ? 'OK' : 'FAILED',
983 $email_test['timing_ms'] );
984
985 if ( ! $email_test['accessible'] && isset( $email_test['access_error'] ) ) {
986 $output .= " Error: " . $email_test['access_error'] . "\n";
987 }
988 }
989 }
990 }
991
992 /**
993 * Format preview test details.
994 *
995 * @since 1.8.9.2
996 *
997 * @param array $test Test results.
998 * @param string &$output Output string to append to.
999 */
1000 private function format_preview_details( $test, &$output ) {
1001 $output .= sprintf( "Functional Previews: %d/%d\n",
1002 $test['functional_count'] ?? 0, $test['details']['total_tested'] ?? 3 );
1003
1004 if ( isset( $test['preview_tests'] ) ) {
1005 $output .= "Preview URL Tests:\n";
1006 foreach ( $test['preview_tests'] as $email_id => $preview_test ) {
1007 $status = $preview_test['preview_works'] ? '✓' : '✗';
1008 $output .= sprintf( " %s %-25s: %s",
1009 $status, $email_id,
1010 $preview_test['preview_works'] ? 'Working' : 'Failed' );
1011
1012 if ( isset( $preview_test['status_code'] ) ) {
1013 $output .= sprintf( " (HTTP %d)", $preview_test['status_code'] );
1014 }
1015 $output .= "\n";
1016
1017 if ( ! $preview_test['preview_works'] && isset( $preview_test['endpoint_error'] ) ) {
1018 $output .= " Error: " . $preview_test['endpoint_error'] . "\n";
1019 }
1020 }
1021
1022 // If all failures are the local SSL certificate error, add a short explanation.
1023 $ssl_error_phrase = 'unable to get local issuer certificate';
1024 $has_ssl_error = false;
1025 $any_failure = false;
1026 foreach ( $test['preview_tests'] as $preview_test ) {
1027 if ( ! $preview_test['preview_works'] ) {
1028 $any_failure = true;
1029 $err = isset( $preview_test['endpoint_error'] ) ? $preview_test['endpoint_error'] : '';
1030 if ( strpos( $err, $ssl_error_phrase ) !== false || strpos( $err, 'cURL error 60' ) !== false ) {
1031 $has_ssl_error = true;
1032 break;
1033 }
1034 }
1035 }
1036 if ( $any_failure && $has_ssl_error ) {
1037 $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";
1038 }
1039 }
1040 }
1041
1042 /**
1043 * Format timing test details.
1044 *
1045 * @since 1.8.9.2
1046 *
1047 * @param array $test Test results.
1048 * @param string &$output Output string to append to.
1049 */
1050 private function format_timing_details( $test, &$output ) {
1051 $details = $test['details'];
1052
1053 $output .= sprintf( "Init Completed: %s\n", $details['init_completed'] ? 'Yes' : 'No' );
1054 $output .= sprintf( "Admin Init Completed: %s\n", $details['admin_init_completed'] ? 'Yes' : 'No' );
1055 $output .= sprintf( "Current Action: %s\n", $details['current_action'] ?? 'none' );
1056
1057 if ( isset( $details['charitable_plugin_position'] ) ) {
1058 $output .= sprintf( "Plugin Load Position: %d/%d\n",
1059 $details['charitable_plugin_position'] + 1, $details['total_active_plugins'] );
1060 }
1061
1062 if ( ! empty( $details['timing_issues'] ) ) {
1063 $output .= "Timing Issues:\n";
1064 foreach ( $details['timing_issues'] as $issue ) {
1065 $output .= " ⚠ {$issue}\n";
1066 }
1067 }
1068 }
1069
1070 /**
1071 * Format environment test details.
1072 *
1073 * @since 1.8.9.2
1074 *
1075 * @param array $test Test results.
1076 * @param string &$output Output string to append to.
1077 */
1078 private function format_environment_details( $test, &$output ) {
1079 $env = $test['environment_info'];
1080
1081 $output .= sprintf( "PHP Version: %s\n", $env['php_version'] );
1082 $output .= sprintf( "WordPress Version: %s\n", $env['wordpress_version'] );
1083 $output .= sprintf( "Charitable Version: %s\n", $env['charitable_version'] );
1084 $output .= sprintf( "Memory Limit: %s\n", $env['memory_limit'] );
1085 $output .= sprintf( "Debug Mode: %s\n", $env['debug_enabled'] ? 'Enabled' : 'Disabled' );
1086
1087 if ( ! empty( $test['caching_plugins'] ) ) {
1088 $output .= "Caching Plugins: " . implode( ', ', $test['caching_plugins'] ) . "\n";
1089 }
1090
1091 $output .= sprintf( "Object Cache: %s\n",
1092 $test['details']['object_cache_enabled'] ? 'Enabled' : 'Disabled' );
1093 $output .= sprintf( "OPcache: %s\n",
1094 $test['details']['opcache_enabled'] ? 'Enabled' : 'Disabled' );
1095 $output .= sprintf( "SMTP Configured: %s\n",
1096 $test['details']['smtp_configured'] ? 'Yes' : 'No' );
1097 }
1098
1099 /**
1100 * Format performance test details.
1101 *
1102 * @since 1.8.9.2
1103 *
1104 * @param array $test Test results.
1105 * @param string &$output Output string to append to.
1106 */
1107 private function format_performance_details( $test, &$output ) {
1108 $metrics = $test['metrics'];
1109
1110 $output .= sprintf( "Helper Access Time: %.2fms\n", $metrics['helper_access_time_ms'] );
1111 $output .= sprintf( "Registration Time: %.2fms\n", $metrics['registration_time_ms'] );
1112 $output .= sprintf( "Total Test Time: %.2fms\n", $metrics['total_time_ms'] );
1113 $output .= sprintf( "Memory Used: %.1fKB\n", $metrics['memory_used_kb'] );
1114 $output .= sprintf( "Peak Memory: %.1fMB\n", $metrics['peak_memory_mb'] );
1115
1116 if ( isset( $test['details']['slow_helper'] ) ) {
1117 $output .= "⚠ " . $test['details']['slow_helper'] . "\n";
1118 }
1119 if ( isset( $test['details']['slow_registration'] ) ) {
1120 $output .= "⚠ " . $test['details']['slow_registration'] . "\n";
1121 }
1122 if ( isset( $test['details']['high_memory'] ) ) {
1123 $output .= "⚠ " . $test['details']['high_memory'] . "\n";
1124 }
1125 }
1126
1127 /**
1128 * AJAX handler for sending test email.
1129 *
1130 * @since 1.8.9.2
1131 *
1132 * @return void
1133 */
1134 public function ajax_send_test_email() {
1135 // Check nonce
1136 if ( ! wp_verify_nonce( $_POST['nonce'], 'charitable_send_test_email' ) ) {
1137 wp_die( __( 'Security check failed', 'charitable' ) );
1138 }
1139
1140 // Check capability
1141 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
1142 wp_die( __( 'Insufficient permissions', 'charitable' ) );
1143 }
1144
1145 try {
1146 // Load the test email class if not already loaded
1147 if ( ! class_exists( 'Charitable_Email_Test' ) ) {
1148 require_once charitable()->get_path( 'includes' ) . 'emails/class-charitable-email-test.php';
1149 }
1150
1151 // Send test email
1152 $result = Charitable_Email_Test::send_test_email();
1153
1154 if ( $result['success'] ) {
1155 wp_send_json_success( array(
1156 'message' => $result['message'],
1157 'success' => true,
1158 ) );
1159 } else {
1160 wp_send_json_error( array(
1161 'message' => $result['message'],
1162 'success' => false,
1163 ) );
1164 }
1165
1166 } catch ( Exception $e ) {
1167 wp_send_json_error( array(
1168 'message' => sprintf(
1169 __( 'Test email failed: %s', 'charitable' ),
1170 $e->getMessage()
1171 ),
1172 'success' => false,
1173 'error_details' => array(
1174 'file' => $e->getFile(),
1175 'line' => $e->getLine(),
1176 ),
1177 ) );
1178 }
1179 }
1180
1181 /**
1182 * Get donation form error logs.
1183 *
1184 * @since 1.8.9.2
1185 *
1186 * @return string
1187 */
1188 private function donation_error_logs() {
1189 $data = "\n\n" . '-- Donation Form Error Logs' . "\n\n";
1190
1191 // Check if error logging is enabled
1192 if ( ! apply_filters( 'charitable_form_error_logging_enabled', true ) ) {
1193 $data .= 'Error logging is disabled.' . "\n";
1194 return $data;
1195 }
1196
1197 // Get recent errors (last 20)
1198 $recent_errors = charitable_get_recent_form_errors( 20 );
1199
1200 if ( empty( $recent_errors ) ) {
1201 $data .= 'No donation form errors found in the last 7 days.' . "\n";
1202 return $data;
1203 }
1204
1205 $data .= 'Recent Errors (last 20):' . "\n";
1206 $data .= str_repeat( '-', 40 ) . "\n";
1207
1208 foreach ( $recent_errors as $error ) {
1209 $data .= sprintf(
1210 '[%s] %s - %s',
1211 $error->date_recorded,
1212 $error->primary_action,
1213 $error->secondary_action
1214 );
1215
1216 // Add context if available
1217 if ( $error->campaign_id ) {
1218 $campaign_title = get_the_title( $error->campaign_id );
1219 $data .= sprintf( ' | Campaign: %s (#%d)', $campaign_title, $error->campaign_id );
1220 }
1221
1222 if ( $error->amount ) {
1223 $data .= sprintf( ' | Amount: %s', charitable_format_money( $error->amount ) );
1224 }
1225
1226 if ( ! empty( $error->meta_data ) && isset( $error->meta_data['gateway'] ) ) {
1227 $data .= sprintf( ' | Gateway: %s', $error->meta_data['gateway'] );
1228 }
1229
1230 // Show error message detail (SMTP errors, exception messages, etc.)
1231 if ( ! empty( $error->meta_data ) && isset( $error->meta_data['error_message'] ) ) {
1232 $data .= "\n → " . $error->meta_data['error_message'];
1233 }
1234
1235 // Show email class for email failures
1236 if ( ! empty( $error->meta_data ) && isset( $error->meta_data['email_class'] ) ) {
1237 $data .= ' [' . $error->meta_data['email_class'] . ']';
1238 }
1239
1240 $data .= "\n";
1241 }
1242
1243 // Get error statistics
1244 $error_stats = charitable_get_form_error_stats( 7 );
1245
1246 if ( ! empty( $error_stats ) ) {
1247 $data .= "\n" . 'Error Summary (last 7 days):' . "\n";
1248 $data .= str_repeat( '-', 40 ) . "\n";
1249
1250 foreach ( $error_stats as $stat ) {
1251 $data .= sprintf(
1252 '%s - %s: %d occurrences (last: %s)' . "\n",
1253 $stat->primary_action,
1254 $stat->secondary_action,
1255 $stat->error_count,
1256 $stat->last_occurrence
1257 );
1258 }
1259 }
1260
1261 return $data;
1262 }
1263
1264 /**
1265 * Get debug log scanner summary for system info.
1266 *
1267 * @since 1.8.9.2
1268 *
1269 * @return string
1270 */
1271 private function debug_log_scanner() {
1272 try {
1273 $scanner = Charitable_Tools_Debug_Log_Scanner::get_instance();
1274 return $scanner->get_debug_log_scanner_summary();
1275 } catch ( Exception $e ) {
1276 return "\n-- Debug Log Scanner\n\nError: Unable to run debug log scanner - " . $e->getMessage() . "\n";
1277 }
1278 }
1279
1280 /**
1281 * Clear error logs via AJAX.
1282 *
1283 * @since 1.8.9.2
1284 *
1285 * @return void
1286 */
1287 public function ajax_clear_error_logs() {
1288 // Check nonce first (distinct message for debugging).
1289 if ( ! check_ajax_referer( 'charitable-admin-tools', 'nonce', false ) ) {
1290 wp_send_json_error( array(
1291 'message' => __( 'Security check failed: invalid or expired nonce. Refresh the page and try again.', 'charitable' ),
1292 'success' => false,
1293 ) );
1294 return;
1295 }
1296 // Check capability.
1297 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
1298 wp_send_json_error( array(
1299 'message' => __( 'You do not have the manage_charitable_settings capability.', 'charitable' ),
1300 'success' => false,
1301 ) );
1302 return;
1303 }
1304
1305 try {
1306 // Clear error logs (delete all)
1307 $deleted_count = charitable_cleanup_old_form_errors( 0 ); // 0 days = delete all
1308
1309 wp_send_json_success( array(
1310 'message' => sprintf(
1311 /* translators: %d: number of deleted error logs */
1312 _n(
1313 '%d error log cleared successfully.',
1314 '%d error logs cleared successfully.',
1315 $deleted_count,
1316 'charitable'
1317 ),
1318 $deleted_count
1319 ),
1320 'success' => true,
1321 'deleted_count' => $deleted_count,
1322 ) );
1323
1324 } catch ( Exception $e ) {
1325 wp_send_json_error( array(
1326 'message' => sprintf(
1327 __( 'Error clearing logs: %s', 'charitable' ),
1328 $e->getMessage()
1329 ),
1330 'success' => false,
1331 ) );
1332 }
1333 }
1334
1335 /**
1336 * Export error logs via AJAX.
1337 *
1338 * @since 1.8.9.2
1339 *
1340 * @return void
1341 */
1342 public function ajax_export_error_logs() {
1343 // Check nonce first (distinct message for debugging).
1344 if ( ! check_ajax_referer( 'charitable-admin-tools', 'nonce', false ) ) {
1345 wp_send_json_error( array(
1346 'message' => __( 'Security check failed: invalid or expired nonce. Refresh the page and try again.', 'charitable' ),
1347 'success' => false,
1348 ) );
1349 return;
1350 }
1351 // Check capability.
1352 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
1353 wp_send_json_error( array(
1354 'message' => __( 'You do not have the manage_charitable_settings capability.', 'charitable' ),
1355 'success' => false,
1356 ) );
1357 return;
1358 }
1359
1360 try {
1361 // Get all error logs
1362 $errors = charitable_get_recent_form_errors( 1000 ); // Get up to 1000 errors
1363
1364 $csv_data = array();
1365 $csv_data[] = array( 'Date', 'Error Type', 'Error Details', 'Campaign ID', 'Campaign Title', 'Amount', 'Gateway', 'User ID' );
1366
1367 foreach ( $errors as $error ) {
1368 $campaign_title = $error->campaign_id ? get_the_title( $error->campaign_id ) : '';
1369 $gateway = ! empty( $error->meta_data ) && isset( $error->meta_data['gateway'] ) ? $error->meta_data['gateway'] : '';
1370
1371 $csv_data[] = array(
1372 $error->date_recorded,
1373 $error->primary_action,
1374 $error->secondary_action,
1375 $error->campaign_id ?: '',
1376 $campaign_title,
1377 $error->amount ? charitable_format_money( $error->amount ) : '',
1378 $gateway,
1379 $error->created_by ?: '',
1380 );
1381 }
1382
1383 // Create CSV content
1384 $csv_content = '';
1385 foreach ( $csv_data as $row ) {
1386 $csv_content .= '"' . implode( '","', $row ) . '"' . "\n";
1387 }
1388
1389 // Prepare download data
1390 $filename = 'charitable-error-logs-' . gmdate( 'Y-m-d-H-i-s' ) . '.csv';
1391
1392 wp_send_json_success( array(
1393 'message' => __( 'Error logs exported successfully.', 'charitable' ),
1394 'success' => true,
1395 'filename' => $filename,
1396 'content' => base64_encode( $csv_content ),
1397 'mimetype' => 'text/csv',
1398 ) );
1399
1400 } catch ( Exception $e ) {
1401 wp_send_json_error( array(
1402 'message' => sprintf(
1403 __( 'Error exporting logs: %s', 'charitable' ),
1404 $e->getMessage()
1405 ),
1406 'success' => false,
1407 ) );
1408 }
1409 }
1410
1411 /**
1412 * AJAX handler for debug log scanner.
1413 *
1414 * @since 1.8.9.2
1415 *
1416 * @return void
1417 */
1418 public function ajax_debug_log_scan() {
1419 // Check nonce
1420 if ( ! wp_verify_nonce( $_POST['nonce'], 'charitable-admin-tools' ) ) {
1421 wp_die( __( 'Security check failed', 'charitable' ) );
1422 }
1423
1424 // Check capability
1425 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
1426 wp_die( __( 'Insufficient permissions', 'charitable' ) );
1427 }
1428
1429 try {
1430 $scanner = Charitable_Tools_Debug_Log_Scanner::get_instance();
1431 $results = $scanner->get_detailed_debug_log_scan();
1432
1433 // Format for display
1434 $output = $this->format_debug_log_scan( $results );
1435
1436 wp_send_json_success( array(
1437 'scan_results' => $output,
1438 'raw_data' => $results
1439 ) );
1440
1441 } catch ( Exception $e ) {
1442 // Enhanced error debugging
1443 $debug_info = array();
1444
1445 // Get debug logging status
1446 $debug_info['wp_debug'] = defined( 'WP_DEBUG' ) ? ( WP_DEBUG ? 'enabled' : 'disabled' ) : 'not defined';
1447 $debug_info['wp_debug_log'] = defined( 'WP_DEBUG_LOG' ) ? ( WP_DEBUG_LOG ? 'enabled' : 'disabled' ) : 'not defined';
1448
1449 // Get log file path
1450 try {
1451 $reflection = new ReflectionClass( $scanner );
1452 $method = $reflection->getMethod( 'get_debug_log_path' );
1453 $method->setAccessible( true );
1454 $log_file = $method->invoke( $scanner );
1455 $debug_info['log_file_path'] = $log_file;
1456 $debug_info['log_file_exists'] = $log_file ? file_exists( $log_file ) : false;
1457 if ( $log_file && file_exists( $log_file ) ) {
1458 $debug_info['log_file_size'] = round( filesize( $log_file ) / 1024, 2 ) . ' KB';
1459 $debug_info['log_file_readable'] = is_readable( $log_file );
1460 }
1461 } catch ( Exception $path_error ) {
1462 $debug_info['log_path_error'] = $path_error->getMessage();
1463 }
1464
1465 // Get memory info
1466 $debug_info['php_memory_limit'] = ini_get( 'memory_limit' );
1467 $debug_info['current_memory_usage'] = round( memory_get_usage( true ) / 1024 / 1024, 2 ) . ' MB';
1468
1469 wp_send_json_error( array(
1470 'message' => 'Debug log scan failed: ' . $e->getMessage(),
1471 'detailed_message' => $this->format_debug_error_message( $e, $debug_info ),
1472 'error_details' => array(
1473 'file' => $e->getFile(),
1474 'line' => $e->getLine(),
1475 'debug_info' => $debug_info
1476 )
1477 ) );
1478 }
1479 }
1480
1481 /**
1482 * Format debug log scan results for display.
1483 *
1484 * @since 1.8.9.2
1485 *
1486 * @param array $results Raw scan results.
1487 * @return array
1488 */
1489 private function format_debug_log_scan( $results ) {
1490 if ( isset( $results['error'] ) ) {
1491 return array(
1492 'summary' => "### DEBUG LOG SCAN ERROR ###\n\n" . $results['error']['message'] . "\n",
1493 'technical' => $results['error']['message'],
1494 'forum_ready' => "Debug log scan encountered an error. Please check if WP_DEBUG and WP_DEBUG_LOG are properly configured."
1495 );
1496 }
1497
1498 if ( ! isset( $results['pre_flight'] ) || ! $results['pre_flight']['passed'] ) {
1499 $messages = isset( $results['pre_flight']['messages'] ) ? implode( "\n", $results['pre_flight']['messages'] ) : 'Unknown pre-flight error';
1500 return array(
1501 'summary' => "### DEBUG LOG SCAN UNAVAILABLE ###\n\n" . $messages . "\n",
1502 'technical' => $messages,
1503 'forum_ready' => "Debug log scanning is not available. " . $messages
1504 );
1505 }
1506
1507 $scanner = Charitable_Tools_Debug_Log_Scanner::get_instance();
1508 $sanitized_errors = $scanner->sanitize_errors_for_sharing( $results['errors'] );
1509
1510 // Generate summary format
1511 $summary = "### DEBUG LOG SCAN RESULTS ###\n\n";
1512 $summary .= sprintf( "Generated: %s\n", $results['timestamp'] ?? 'Unknown' );
1513 $summary .= sprintf( "Execution Time: %sms\n", $results['execution_time'] ?? '0' );
1514 $summary .= sprintf( "Scan Timeframe: %d hours\n\n", $results['scan_timeframe'] ?? 24 );
1515
1516 $summary .= "=== SUMMARY ===\n";
1517 $summary .= sprintf( "Total Errors Found: %d\n", $results['statistics']['total_errors'] );
1518
1519 if ( ! empty( $results['statistics']['by_category'] ) ) {
1520 $summary .= "\nErrors by Category:\n";
1521 foreach ( $results['statistics']['by_category'] as $category => $count ) {
1522 $summary .= sprintf( " %s: %d\n", ucfirst( str_replace( '_', ' ', $category ) ), $count );
1523 }
1524 }
1525
1526 if ( ! empty( $results['statistics']['by_priority'] ) ) {
1527 $summary .= "\nErrors by Priority:\n";
1528 foreach ( $results['statistics']['by_priority'] as $priority => $count ) {
1529 $summary .= sprintf( " %s: %d\n", $priority, $count );
1530 }
1531 }
1532
1533 if ( ! empty( $results['recommendations'] ) ) {
1534 $summary .= "\nRecommendations:\n";
1535 foreach ( $results['recommendations'] as $recommendation ) {
1536 $summary .= " " . $recommendation . "\n";
1537 }
1538 }
1539
1540 // Generate technical details
1541 $technical = $summary . "\n=== DETAILED ERROR LOG ===\n\n";
1542 if ( ! empty( $sanitized_errors ) ) {
1543 foreach ( $sanitized_errors as $error ) {
1544 $technical .= sprintf( "[%s] %s - %s\n", $error['datetime'], $error['level'], $error['category'] );
1545 $technical .= sprintf( "Priority: %d (%s)\n", $error['priority'], $error['description'] );
1546 $technical .= sprintf( "Message: %s\n", $error['message'] );
1547 if ( ! empty( $error['file'] ) ) {
1548 $technical .= sprintf( "File: %s", $error['file'] );
1549 if ( ! empty( $error['line'] ) ) {
1550 $technical .= sprintf( " (line %s)", $error['line'] );
1551 }
1552 $technical .= "\n";
1553 }
1554 $technical .= "\n" . str_repeat( '-', 50 ) . "\n\n";
1555 }
1556 } else {
1557 $technical .= "No errors found in the specified timeframe.\n";
1558 }
1559
1560 // Generate forum-ready format
1561 $forum_ready = "### Charitable Debug Log Scan Results ###\n\n";
1562 $forum_ready .= sprintf( "**WordPress Version:** %s\n", get_bloginfo( 'version' ) );
1563 $forum_ready .= sprintf( "**PHP Version:** %s\n", PHP_VERSION );
1564 $forum_ready .= sprintf( "**Charitable Version:** %s\n\n", charitable()->get_version() );
1565
1566 $forum_ready .= sprintf( "**Scan Summary:** %d errors found in last %d hours\n\n",
1567 $results['statistics']['total_errors'],
1568 $results['scan_timeframe'] ?? 24
1569 );
1570
1571 if ( ! empty( $results['statistics']['by_category'] ) ) {
1572 $forum_ready .= "**Error Categories:**\n";
1573 foreach ( $results['statistics']['by_category'] as $category => $count ) {
1574 $forum_ready .= sprintf( "- %s: %d\n", ucfirst( str_replace( '_', ' ', $category ) ), $count );
1575 }
1576 $forum_ready .= "\n";
1577 }
1578
1579 if ( ! empty( $sanitized_errors ) ) {
1580 $forum_ready .= "**Recent Errors (Sanitized):**\n\n";
1581 $error_count = 0;
1582 foreach ( $sanitized_errors as $error ) {
1583 if ( $error_count >= 5 ) { // Limit to top 5 errors for forum sharing
1584 $forum_ready .= sprintf( "... and %d more errors (see technical details)\n\n", count( $sanitized_errors ) - 5 );
1585 break;
1586 }
1587 $forum_ready .= sprintf( "**Error %d:** [%s] %s\n", $error_count + 1, $error['datetime'], $error['level'] );
1588 $forum_ready .= sprintf( "Category: %s (Priority: %d)\n", $error['category'], $error['priority'] );
1589 $forum_ready .= sprintf( "Message: %s\n", $error['message'] );
1590 if ( ! empty( $error['file'] ) ) {
1591 $forum_ready .= sprintf( "File: %s", $error['file'] );
1592 if ( ! empty( $error['line'] ) ) {
1593 $forum_ready .= sprintf( " (line %s)", $error['line'] );
1594 }
1595 $forum_ready .= "\n";
1596 }
1597 $forum_ready .= "\n";
1598 $error_count++;
1599 }
1600 }
1601
1602 if ( ! empty( $results['recommendations'] ) ) {
1603 $forum_ready .= "**Recommendations:**\n";
1604 foreach ( $results['recommendations'] as $recommendation ) {
1605 $forum_ready .= "- " . $recommendation . "\n";
1606 }
1607 }
1608
1609 return array(
1610 'summary' => $summary,
1611 'technical' => $technical,
1612 'forum_ready' => $forum_ready
1613 );
1614 }
1615
1616 /**
1617 * Format detailed debug error message.
1618 *
1619 * @since 1.8.9.2
1620 *
1621 * @param Exception $exception The exception that occurred.
1622 * @param array $debug_info Debug information array.
1623 * @return string
1624 */
1625 private function format_debug_error_message( $exception, $debug_info ) {
1626 $message = "Debug Log Scanner Error Details:\n\n";
1627
1628 $message .= "Error: " . $exception->getMessage() . "\n";
1629 $message .= "File: " . $exception->getFile() . ":" . $exception->getLine() . "\n\n";
1630
1631 $message .= "WordPress Debug Configuration:\n";
1632 $message .= "- WP_DEBUG: " . $debug_info['wp_debug'] . "\n";
1633 $message .= "- WP_DEBUG_LOG: " . $debug_info['wp_debug_log'] . "\n\n";
1634
1635 if ( isset( $debug_info['log_file_path'] ) ) {
1636 $message .= "Log File Information:\n";
1637 $message .= "- Path: " . $debug_info['log_file_path'] . "\n";
1638 $message .= "- Exists: " . ( $debug_info['log_file_exists'] ? 'Yes' : 'No' ) . "\n";
1639 if ( isset( $debug_info['log_file_size'] ) ) {
1640 $message .= "- Size: " . $debug_info['log_file_size'] . "\n";
1641 $message .= "- Readable: " . ( $debug_info['log_file_readable'] ? 'Yes' : 'No' ) . "\n";
1642 }
1643 $message .= "\n";
1644 }
1645
1646 $message .= "System Information:\n";
1647 $message .= "- PHP Memory Limit: " . $debug_info['php_memory_limit'] . "\n";
1648 $message .= "- Current Memory Usage: " . $debug_info['current_memory_usage'] . "\n\n";
1649
1650 // Provide helpful suggestions
1651 $message .= "Troubleshooting Suggestions:\n";
1652
1653 if ( $debug_info['wp_debug'] !== 'enabled' || $debug_info['wp_debug_log'] !== 'enabled' ) {
1654 $message .= "1. Enable debug logging in wp-config.php:\n";
1655 $message .= " define('WP_DEBUG', true);\n";
1656 $message .= " define('WP_DEBUG_LOG', true);\n\n";
1657 }
1658
1659 if ( isset( $debug_info['log_file_exists'] ) && ! $debug_info['log_file_exists'] ) {
1660 $message .= "2. No debug.log file found - this is normal if no errors have occurred.\n";
1661 $message .= " Try creating a test error or reproduce an issue first.\n\n";
1662 }
1663
1664 if ( isset( $debug_info['log_file_readable'] ) && ! $debug_info['log_file_readable'] ) {
1665 $message .= "3. Debug log file is not readable - check file permissions.\n\n";
1666 }
1667
1668 $message .= "4. If the issue persists, copy this detailed error message when reporting the issue.";
1669
1670 return $message;
1671 }
1672
1673 /**
1674 * Returns and/or create the single instance of this class.
1675 *
1676 * @since 1.8.1.6
1677 *
1678 * @return Charitable_Tools_Settings
1679 */
1680 public static function get_instance() {
1681 if ( is_null( self::$instance ) ) {
1682 self::$instance = new self();
1683 }
1684
1685 return self::$instance;
1686 }
1687
1688 /**
1689 * Hosting Environment, Filesystem, and recent Plugin Install error details.
1690 *
1691 * Designed to surface the signals that matter most when troubleshooting
1692 * plugin upload/install failures on managed hosts (WordPress.com, WP
1693 * Engine, Kinsta, etc.) where standard PHP limits are misleading.
1694 *
1695 * @since 1.8.10.5
1696 *
1697 * @return string
1698 */
1699 private function hosting_environment_info() {
1700
1701 $data = "\n" . '-- Hosting Environment' . "\n\n";
1702 $data .= 'Detected Host: ' . self::detect_host() . "\n";
1703 $data .= 'Reverse Proxy/CDN: ' . self::detect_proxy() . "\n";
1704
1705 $data .= "\n" . '-- Filesystem & Updates' . "\n\n";
1706 $data .= 'DISALLOW_FILE_EDIT: ' . self::format_constant_state( 'DISALLOW_FILE_EDIT' ) . "\n";
1707 $data .= 'DISALLOW_FILE_MODS: ' . self::format_constant_state( 'DISALLOW_FILE_MODS' ) . "\n";
1708 $data .= 'AUTOMATIC_UPDATER_DISABLED: ' . self::format_constant_state( 'AUTOMATIC_UPDATER_DISABLED' ) . "\n";
1709 $data .= 'WP_AUTO_UPDATE_CORE: ' . self::format_constant_state( 'WP_AUTO_UPDATE_CORE' ) . "\n";
1710 $data .= 'FS_METHOD: ' . ( defined( 'FS_METHOD' ) ? esc_html( (string) FS_METHOD ) : '(auto-detect)' ) . "\n";
1711 $data .= 'Plugin Dir Writable: ' . ( is_writable( WP_PLUGIN_DIR ) ? 'Yes' : 'No' ) . ' (' . WP_PLUGIN_DIR . ')' . "\n";
1712 $data .= 'wp-content Writable: ' . ( is_writable( WP_CONTENT_DIR ) ? 'Yes' : 'No' ) . "\n";
1713
1714 // Note about misleading PHP limits on managed hosts.
1715 if ( self::is_known_managed_host() ) {
1716 $data .= "\n" . 'Note: PHP upload/memory limits shown above reflect OS-level config.' . "\n";
1717 $data .= 'Managed hosts may enforce stricter platform-level caps that override them.' . "\n";
1718 }
1719
1720 // Recent install errors (captured by the upgrader_install_package_result filter).
1721 $errors = (array) get_option( 'charitable_recent_install_errors', array() );
1722 if ( ! empty( $errors ) ) {
1723 $data .= "\n" . '-- Recent Plugin Install/Update Errors (last 5)' . "\n\n";
1724 foreach ( array_reverse( $errors ) as $err ) {
1725 $ts = isset( $err['time'] ) ? gmdate( 'Y-m-d H:i:s', (int) $err['time'] ) . ' UTC' : '?';
1726 $action = isset( $err['action'] ) ? $err['action'] : '?';
1727 $plugin = isset( $err['plugin'] ) && $err['plugin'] ? $err['plugin'] : '(unknown)';
1728 $code = isset( $err['code'] ) ? $err['code'] : '?';
1729 $msg = isset( $err['message'] ) ? $err['message'] : '';
1730 $data .= '[' . $ts . '] ' . $action . ' ' . $plugin . "\n";
1731 $data .= ' Code: ' . $code . "\n";
1732 $data .= ' Message: ' . wp_strip_all_tags( $msg ) . "\n";
1733 }
1734 }
1735
1736 return $data;
1737 }
1738
1739 /**
1740 * Detect the hosting platform.
1741 *
1742 * Read-only: only checks defined constants, loaded classes, and
1743 * already-installed mu-plugin filenames. No filesystem scanning beyond
1744 * the existing get_mu_plugins() result, no network calls.
1745 *
1746 * @since 1.8.10.5
1747 *
1748 * @return string Human-readable host description, or "Unknown / generic".
1749 */
1750 private static function detect_host() {
1751
1752 // Cache the result — this is called at least twice per render
1753 // (once directly, once via is_known_managed_host()).
1754 static $cached = null;
1755 if ( null !== $cached ) {
1756 return $cached;
1757 }
1758
1759 // WordPress.com (Atomic infrastructure).
1760 $is_wpcom = defined( 'WPCOMSH_VERSION' )
1761 || defined( 'IS_ATOMIC' )
1762 || defined( 'IS_WPCOM' );
1763
1764 if ( ! $is_wpcom && function_exists( 'get_mu_plugins' ) ) {
1765 $mu = get_mu_plugins();
1766 if ( isset( $mu['wpcomsh-loader.php'] ) ) {
1767 $is_wpcom = true;
1768 }
1769 }
1770
1771 if ( $is_wpcom ) {
1772 $cached = 'WordPress.com (' . self::detect_wpcom_plan() . ')';
1773 } elseif ( defined( 'WPE_APIKEY' ) || defined( 'IS_WPE' ) || defined( 'WPE_ATLAS_PLUGIN_VERSION' ) ) {
1774 $cached = 'WP Engine';
1775 } elseif ( defined( 'KINSTAMU_VERSION' ) || defined( 'KINSTA_CACHE_ZONE' ) || class_exists( 'Kinsta\\Cache' ) ) {
1776 $cached = 'Kinsta';
1777 } elseif ( defined( 'PANTHEON_ENVIRONMENT' ) || isset( $_SERVER['PANTHEON_ENVIRONMENT'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
1778 $cached = 'Pantheon';
1779 } elseif ( defined( 'IS_PRESSABLE' ) || defined( 'PRESSABLE_VERSION' ) ) {
1780 $cached = 'Pressable';
1781 } elseif ( defined( 'CLOUDWAYS_VERSION' ) || isset( $_SERVER['cw_allowed_ip'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
1782 $cached = 'Cloudways';
1783 } elseif ( defined( 'GD_SYSTEM_PLUGIN_DIR' ) || class_exists( 'WPaaS\\Plugin' ) ) {
1784 $cached = 'GoDaddy Managed WordPress';
1785 } elseif ( defined( 'MM_BASE_DIR' ) || class_exists( 'EPC_Loader' ) ) {
1786 $cached = 'Bluehost / Endurance';
1787 } elseif ( defined( 'DH_LITESPEED_PLUGIN' ) || isset( $_SERVER['DH_USER'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
1788 $cached = 'DreamHost (DreamPress)';
1789 } elseif ( class_exists( 'SiteGround_Optimizer\\Loader' ) || class_exists( 'SG_CachePress\\Loader' ) ) {
1790 $cached = 'SiteGround';
1791 } elseif ( defined( 'FLYWHEEL_CONFIG_DIR' ) || defined( 'FLYWHEEL_PLUGIN_DIR' ) ) {
1792 $cached = 'Flywheel';
1793 } elseif ( defined( 'HOSTINGER_LOCAL_BIN' ) || defined( 'HOSTINGER_AMP_INSTALLED' ) ) {
1794 $cached = 'Hostinger';
1795 } else {
1796 $cached = 'Unknown / generic';
1797 }
1798
1799 return $cached;
1800 }
1801
1802 /**
1803 * Detect WordPress.com plan tier where possible.
1804 *
1805 * Tries several known surfaces in order; returns "plan unknown" if none
1806 * are accessible (WP.com hides this info from most plugin contexts).
1807 *
1808 * @since 1.8.10.5
1809 *
1810 * @return string
1811 */
1812 private static function detect_wpcom_plan() {
1813
1814 // Direct constant (rare, but possible).
1815 if ( defined( 'WPCOMSH_PLAN' ) && constant( 'WPCOMSH_PLAN' ) ) {
1816 return (string) constant( 'WPCOMSH_PLAN' ) . ' plan';
1817 }
1818
1819 // Jetpack Plan API: present on WP.com sites because Jetpack is bundled.
1820 // Wrapped in try/catch because Jetpack_Plan::get() has historically
1821 // thrown on partially-initialised Jetpack states, and System Info
1822 // must never fatal.
1823 if ( class_exists( 'Jetpack_Plan' ) && method_exists( 'Jetpack_Plan', 'get' ) ) {
1824 try {
1825 $plan = Jetpack_Plan::get();
1826 if ( is_array( $plan ) ) {
1827 if ( ! empty( $plan['product_name_short'] ) ) {
1828 return $plan['product_name_short'] . ' plan';
1829 }
1830 if ( ! empty( $plan['product_slug'] ) ) {
1831 return $plan['product_slug'] . ' plan';
1832 }
1833 }
1834 } catch ( \Throwable $e ) { // phpcs:ignore
1835 // Fall through to "plan unknown" on any Jetpack internal error.
1836 unset( $e );
1837 }
1838 }
1839
1840 return 'plan unknown';
1841 }
1842
1843 /**
1844 * Detect a reverse proxy or CDN in front of the site.
1845 *
1846 * @since 1.8.10.5
1847 *
1848 * @return string
1849 */
1850 private static function detect_proxy() {
1851
1852 $signals = array();
1853
1854 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated
1855 if ( ! empty( $_SERVER['HTTP_CF_RAY'] ) || ! empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
1856 $signals[] = 'Cloudflare';
1857 }
1858 if ( ! empty( $_SERVER['HTTP_X_SUCURI_CLIENTIP'] ) ) {
1859 $signals[] = 'Sucuri';
1860 }
1861 if ( ! empty( $_SERVER['HTTP_X_FORWARDED_HOST'] ) && is_string( $_SERVER['HTTP_X_FORWARDED_HOST'] ) ) {
1862 $fwd_host = wp_unslash( $_SERVER['HTTP_X_FORWARDED_HOST'] );
1863 if ( is_string( $fwd_host ) && false !== stripos( $fwd_host, 'cdn' ) ) {
1864 $signals[] = 'CDN (X-Forwarded-Host)';
1865 }
1866 }
1867 if ( ! empty( $_SERVER['HTTP_FASTLY_CLIENT_IP'] ) ) {
1868 $signals[] = 'Fastly';
1869 }
1870 if ( ! empty( $_SERVER['HTTP_X_AKAMAI_EDGESCAPE'] ) ) {
1871 $signals[] = 'Akamai';
1872 }
1873 // phpcs:enable
1874
1875 return empty( $signals ) ? 'None detected' : implode( ', ', $signals );
1876 }
1877
1878 /**
1879 * Whether the current host is one we know enforces platform-level limits.
1880 *
1881 * @since 1.8.10.5
1882 *
1883 * @return bool
1884 */
1885 private static function is_known_managed_host() {
1886
1887 $host = self::detect_host();
1888
1889 $managed = array(
1890 'WordPress.com',
1891 'WP Engine',
1892 'Kinsta',
1893 'Pantheon',
1894 'Pressable',
1895 'GoDaddy Managed WordPress',
1896 'Bluehost / Endurance',
1897 'DreamHost (DreamPress)',
1898 'SiteGround',
1899 'Flywheel',
1900 );
1901
1902 foreach ( $managed as $needle ) {
1903 if ( false !== strpos( $host, $needle ) ) {
1904 return true;
1905 }
1906 }
1907
1908 return false;
1909 }
1910
1911 /**
1912 * Render a "constant defined and truthy" report line in a single string.
1913 *
1914 * @since 1.8.10.5
1915 *
1916 * @param string $name Constant name.
1917 * @return string
1918 */
1919 private static function format_constant_state( $name ) {
1920
1921 if ( ! defined( $name ) ) {
1922 return 'Not defined';
1923 }
1924
1925 $value = constant( $name );
1926
1927 if ( is_bool( $value ) ) {
1928 return 'Defined (' . ( $value ? 'true' : 'false' ) . ')';
1929 }
1930
1931 if ( is_scalar( $value ) ) {
1932 return 'Defined (' . (string) $value . ')';
1933 }
1934
1935 return 'Defined';
1936 }
1937
1938 /**
1939 * Capture failed plugin installs/updates into a small ring buffer.
1940 *
1941 * Hooked on `upgrader_install_package_result`. Pure pass-through on
1942 * success (the filter must return its first argument unchanged in
1943 * all branches). Only records WP_Error results for plugin-type
1944 * operations. Caps storage at 5 most recent errors.
1945 *
1946 * @since 1.8.10.5
1947 *
1948 * @param array|WP_Error $result The install package result.
1949 * @param array $hook_extra Extra args from the upgrader.
1950 * @return array|WP_Error Unchanged result.
1951 */
1952 public static function log_install_result( $result, $hook_extra = array() ) {
1953
1954 // Pass-through: only act on plugin install/update errors.
1955 if ( ! is_wp_error( $result ) ) {
1956 return $result;
1957 }
1958
1959 if ( ! is_array( $hook_extra ) || empty( $hook_extra['type'] ) || 'plugin' !== $hook_extra['type'] ) {
1960 return $result;
1961 }
1962
1963 $entry = array(
1964 'time' => time(),
1965 'action' => isset( $hook_extra['action'] ) ? (string) $hook_extra['action'] : '',
1966 'plugin' => isset( $hook_extra['plugin'] ) ? (string) $hook_extra['plugin'] : '',
1967 'code' => $result->get_error_code(),
1968 'message' => $result->get_error_message(),
1969 );
1970
1971 $errors = (array) get_option( 'charitable_recent_install_errors', array() );
1972 $errors[] = $entry;
1973 $errors = array_slice( $errors, -5 );
1974
1975 update_option( 'charitable_recent_install_errors', $errors, false );
1976
1977 return $result;
1978 }
1979 }
1980
1981 endif;
1982