PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
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 1.7.0.9 All 211 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.13, at includes/admin/tools/class-charitable-tools-system-info.php

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