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-debug-log-scanner.php

class-charitable-tools-debug-log-scanner.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13, at includes/admin/tools/class-charitable-tools-debug-log-scanner.php

918 lines 24.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Tools - Debug Log Scanner.
4 *
5 * @package Charitable/Classes/Charitable_Tools_Debug_Log_Scanner
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.9.2
10 */
11
12 // Exit if accessed directly.
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 if ( ! class_exists( 'Charitable_Tools_Debug_Log_Scanner' ) ) :
18
19 /**
20 * Charitable_Tools_Debug_Log_Scanner
21 *
22 * @final
23 * @since 1.8.9.2
24 */
25 class Charitable_Tools_Debug_Log_Scanner {
26
27 /**
28 * The single instance of this class.
29 *
30 * @since 1.8.9.2
31 *
32 * @var Charitable_Tools_Debug_Log_Scanner|null
33 */
34 private static $instance = null;
35
36 /**
37 * Cache key for scanner results.
38 *
39 * @since 1.8.9.2
40 * @var string
41 */
42 private $cache_key = 'charitable_debug_log_scanner_cache';
43
44 /**
45 * Cache duration in seconds (5 minutes).
46 *
47 * @since 1.8.9.2
48 * @var int
49 */
50 private $cache_duration = 300;
51
52 /**
53 * Maximum file size to process (50MB).
54 *
55 * @since 1.8.9.2
56 * @var int
57 */
58 const MAX_FILE_SIZE_MB = 50;
59
60 /**
61 * Maximum lines to scan for safety.
62 *
63 * @since 1.8.9.2
64 * @var int
65 */
66 const MAX_LINES_TO_SCAN = 10000;
67
68 /**
69 * Read chunk size (1MB).
70 *
71 * @since 1.8.9.2
72 * @var int
73 */
74 const READ_CHUNK_SIZE = 1048576;
75
76 /**
77 * Set up the class.
78 *
79 * @since 1.8.9.2
80 */
81 private function __construct() {
82 // Private constructor for singleton
83 }
84
85 /**
86 * Returns and/or create the single instance of this class.
87 *
88 * @since 1.8.9.2
89 *
90 * @return Charitable_Tools_Debug_Log_Scanner
91 */
92 public static function get_instance() {
93 if ( is_null( self::$instance ) ) {
94 self::$instance = new self();
95 }
96
97 return self::$instance;
98 }
99
100 /**
101 * Get debug log scanner summary for system info display.
102 *
103 * @since 1.8.9.2
104 *
105 * @return string
106 */
107 public function get_debug_log_scanner_summary() {
108 try {
109 $log_file = $this->get_debug_log_path();
110 $scan_info = $this->get_basic_scan_info();
111
112 $data = "\n-- Debug Log Scanner\n\n";
113 $data .= sprintf( "Debug Logging: %s\n", $scan_info['debug_enabled'] ? 'Enabled' : 'Disabled' );
114 $data .= sprintf( "Log File Location: %s\n", $scan_info['log_file_status'] );
115 $data .= sprintf( "Log File Size: %s\n", $scan_info['file_size'] );
116 $data .= sprintf( "Recent Fatal Errors: %d (last 24h)\n", $scan_info['recent_fatals'] );
117 $data .= sprintf( "Charitable Errors: %d (last 24h)\n", $scan_info['charitable_errors'] );
118 $data .= sprintf( "Last Scan: %s\n", $scan_info['last_scan'] );
119
120 return $data;
121
122 } catch ( Exception $e ) {
123 return "\n-- Debug Log Scanner\n\nError: Unable to run debug log scanner - " . $e->getMessage() . "\n";
124 }
125 }
126
127 /**
128 * Get detailed debug log scan for AJAX response.
129 *
130 * @since 1.8.9.2
131 *
132 * @return array
133 */
134 public function get_detailed_debug_log_scan() {
135 // Check for cached results first
136 $cached = get_transient( $this->cache_key );
137 if ( false !== $cached ) {
138 return $cached;
139 }
140
141 $scan_results = array();
142 $start_time = microtime( true );
143
144 try {
145 // Pre-flight checks
146 $scan_results['pre_flight'] = $this->run_pre_flight_checks();
147 if ( ! $scan_results['pre_flight']['passed'] ) {
148 return $scan_results;
149 }
150
151 // Get scan timeframe (filterable)
152 $hours = apply_filters( 'charitable_debug_log_scan_hours', 24 );
153
154 // Scan for errors
155 $scan_results['errors'] = $this->scan_recent_errors( $hours );
156 $scan_results['statistics'] = $this->generate_error_statistics( $scan_results['errors'] );
157 $scan_results['recommendations'] = $this->generate_recommendations( $scan_results );
158
159 // Add timing and metadata
160 $scan_results['execution_time'] = round( ( microtime( true ) - $start_time ) * 1000, 2 );
161 $scan_results['timestamp'] = current_time( 'Y-m-d H:i:s' );
162 $scan_results['scan_timeframe'] = $hours;
163
164 // Cache the results
165 set_transient( $this->cache_key, $scan_results, $this->cache_duration );
166
167 } catch ( Exception $e ) {
168 // Enhanced error information for debugging
169 $error_context = array();
170
171 // Add debug logging status
172 $error_context['wp_debug'] = defined( 'WP_DEBUG' ) && WP_DEBUG;
173 $error_context['wp_debug_log'] = defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG;
174
175 // Add log file information
176 $log_file = $this->get_debug_log_path();
177 $error_context['log_file_path'] = $log_file;
178 $error_context['log_file_exists'] = $log_file ? file_exists( $log_file ) : false;
179
180 // Add memory information
181 $error_context['memory_available'] = $this->check_memory_availability();
182 $error_context['php_memory_limit'] = ini_get( 'memory_limit' );
183
184 $scan_results['error'] = array(
185 'message' => 'Debug log scan failed: ' . $e->getMessage(),
186 'code' => $e->getCode(),
187 'file' => $e->getFile(),
188 'line' => $e->getLine(),
189 'context' => $error_context
190 );
191 }
192
193 return $scan_results;
194 }
195
196 /**
197 * Run pre-flight safety checks.
198 *
199 * @since 1.8.9.2
200 *
201 * @return array
202 */
203 private function run_pre_flight_checks() {
204 $checks = array(
205 'passed' => true,
206 'messages' => array()
207 );
208
209 // Check memory availability
210 if ( ! $this->check_memory_availability() ) {
211 $checks['passed'] = false;
212 $checks['messages'][] = 'Insufficient memory available for log scanning.';
213 }
214
215 // Check debug log file
216 $log_file = $this->get_debug_log_path();
217 if ( ! $log_file ) {
218 $checks['passed'] = false;
219 $checks['messages'][] = 'WordPress debug logging not enabled or log file not found.';
220 return $checks;
221 }
222
223 // Check file safety (if file exists)
224 if ( file_exists( $log_file ) && ! $this->is_file_safe_to_read( $log_file ) ) {
225 $checks['passed'] = false;
226 $checks['messages'][] = 'Debug log file is too large for safe processing (>' . self::MAX_FILE_SIZE_MB . 'MB).';
227 }
228
229 return $checks;
230 }
231
232 /**
233 * Get debug log file path from WordPress configuration.
234 *
235 * @since 1.8.9.2
236 *
237 * @return string|false
238 */
239 private function get_debug_log_path() {
240 $custom_path = null;
241 $default_path = WP_CONTENT_DIR . '/debug.log';
242
243 // Check if WP_DEBUG_LOG specifies a custom path
244 if ( defined( 'WP_DEBUG_LOG' ) && is_string( WP_DEBUG_LOG ) ) {
245 $custom_path = WP_DEBUG_LOG;
246 }
247
248 // If debug logging is properly enabled, return the configured path
249 if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
250 return $custom_path ? $custom_path : $default_path;
251 }
252
253 // Even if debug logging is disabled, check if log files exist
254 // This allows scanning historical data
255 if ( $custom_path && file_exists( $custom_path ) ) {
256 return $custom_path;
257 }
258
259 if ( file_exists( $default_path ) ) {
260 return $default_path;
261 }
262
263 return false;
264 }
265 /**
266 * Get debug log scanner UI state information.
267 *
268 * @since 1.8.9.3
269 *
270 * @return array UI state information
271 */
272 public function get_ui_state() {
273 $debug_enabled = defined( 'WP_DEBUG' ) && WP_DEBUG;
274 $debug_log_enabled = defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG;
275 $log_path = $this->get_debug_log_path();
276 $log_file_exists = $log_path && file_exists( $log_path );
277
278 $state = array(
279 'debug_enabled' => $debug_enabled,
280 'debug_log_enabled' => $debug_log_enabled,
281 'log_path' => $log_path,
282 'log_file_exists' => $log_file_exists,
283 'show_button' => $log_file_exists, // Show button if file exists, regardless of settings
284 'button_enabled' => $log_file_exists, // Enable button if file exists
285 );
286
287 // Determine UI message based on state
288 if ( $debug_enabled && $debug_log_enabled && $log_file_exists ) {
289 $state['status'] = 'ready';
290 $state['message'] = '';
291 } elseif ( $debug_enabled && $debug_log_enabled && ! $log_file_exists ) {
292 $state['status'] = 'no_errors_yet';
293 $state['message'] = __( 'Debug logging is enabled, but no error log file has been created yet. This means no errors have been logged recently.', 'charitable' );
294 } elseif ( ! $debug_enabled || ! $debug_log_enabled ) {
295 if ( $log_file_exists ) {
296 $state['status'] = 'disabled_but_file_exists';
297 $state['message'] = __( 'Debug logging is disabled, but scanning existing log file.', 'charitable' );
298 } else {
299 $state['status'] = 'disabled_no_file';
300 $state['message'] = __( 'Debug logging is not enabled.', 'charitable' );
301 $state['show_button'] = false;
302 $state['button_enabled'] = false;
303 }
304 }
305
306 return $state;
307 }
308 /**
309 * Check if file is safe to read.
310 *
311 * @since 1.8.9.2
312 *
313 * @param string $file_path Path to the log file.
314 * @return bool
315 */
316 private function is_file_safe_to_read( $file_path ) {
317 if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
318 return false;
319 }
320
321 $file_size_mb = filesize( $file_path ) / 1024 / 1024;
322 return $file_size_mb <= self::MAX_FILE_SIZE_MB;
323 }
324
325 /**
326 * Check memory availability for processing.
327 *
328 * @since 1.8.9.2
329 *
330 * @return bool
331 */
332 private function check_memory_availability() {
333 $memory_limit = ini_get( 'memory_limit' );
334 $memory_usage = memory_get_usage( true );
335
336 // Convert memory_limit to bytes
337 $limit_bytes = $this->convert_to_bytes( $memory_limit );
338 $available = $limit_bytes - $memory_usage;
339
340 // Need at least 10MB available to proceed safely
341 return $available >= ( 10 * 1024 * 1024 );
342 }
343
344 /**
345 * Convert memory limit string to bytes.
346 *
347 * @since 1.8.9.2
348 *
349 * @param string $memory_limit Memory limit string (e.g., '256M').
350 * @return int
351 */
352 private function convert_to_bytes( $memory_limit ) {
353 $unit = strtoupper( substr( $memory_limit, -1 ) );
354 $value = (int) $memory_limit;
355
356 switch ( $unit ) {
357 case 'G':
358 return $value * 1024 * 1024 * 1024;
359 case 'M':
360 return $value * 1024 * 1024;
361 case 'K':
362 return $value * 1024;
363 default:
364 return $value;
365 }
366 }
367
368 /**
369 * Scan for recent errors in the debug log.
370 *
371 * @since 1.8.9.2
372 *
373 * @param int $hours Hours to look back.
374 * @return array
375 */
376 private function scan_recent_errors( $hours ) {
377 $log_file = $this->get_debug_log_path();
378 $target_timestamp = time() - ( $hours * 3600 );
379 $errors = array();
380
381 // If log file doesn't exist yet, return empty array (this is normal)
382 if ( ! file_exists( $log_file ) ) {
383 return $errors;
384 }
385
386 try {
387 $handle = fopen( $log_file, 'r' );
388 if ( ! $handle ) {
389 return $errors;
390 }
391
392 // Get file size and start from end
393 fseek( $handle, 0, SEEK_END );
394 $file_size = ftell( $handle );
395
396 $lines_read = 0;
397 $current_line = '';
398 $pos = $file_size - 1;
399
400 // Read backwards from end of file
401 while ( $pos >= 0 && $lines_read < self::MAX_LINES_TO_SCAN ) {
402 fseek( $handle, $pos );
403 $char = fgetc( $handle );
404
405 if ( $char === "\n" ) {
406 $lines_read++;
407
408 if ( ! empty( $current_line ) ) {
409 $parsed_entry = $this->parse_log_entry( strrev( $current_line ) );
410
411 if ( $parsed_entry ) {
412 if ( $parsed_entry['timestamp'] >= $target_timestamp ) {
413 $classification = $this->classify_log_entry( $parsed_entry );
414 if ( $classification ) {
415 $parsed_entry = array_merge( $parsed_entry, $classification );
416 $errors[] = $parsed_entry;
417 }
418 } elseif ( $parsed_entry['timestamp'] < $target_timestamp ) {
419 // Reached our time limit, stop reading
420 break;
421 }
422 }
423 }
424 $current_line = '';
425 } else {
426 $current_line .= $char;
427 }
428
429 $pos--;
430 }
431
432 fclose( $handle );
433
434 } catch ( Exception $e ) {
435 if ( charitable_is_debug() ) {
436 error_log( 'Charitable debug log scanner error: ' . $e->getMessage() );
437 }
438 }
439
440 // Sort chronologically (newest first)
441 usort( $errors, function( $a, $b ) {
442 return $b['timestamp'] - $a['timestamp'];
443 } );
444
445 return $errors;
446 }
447
448 /**
449 * Parse a single log entry.
450 *
451 * @since 1.8.9.2
452 *
453 * @param string $line Log line to parse.
454 * @return array|null
455 */
456 private function parse_log_entry( $line ) {
457 // Match standard WordPress debug log format
458 $pattern = '/\[(\d{2}-\w{3}-\d{4} \d{2}:\d{2}:\d{2} UTC)\] PHP (.+?):(.*)/';
459
460 if ( preg_match( $pattern, $line, $matches ) ) {
461 $timestamp = strtotime( $matches[1] );
462 $level = trim( $matches[2] );
463 $message = trim( $matches[3] );
464
465 // Extract file and line information if present
466 $file = '';
467 $line_number = '';
468 if ( preg_match( '/in (.+?) on line (\d+)/', $message, $file_matches ) ) {
469 $file = $file_matches[1];
470 $line_number = $file_matches[2];
471 }
472
473 return array(
474 'timestamp' => $timestamp,
475 'datetime' => date( 'Y-m-d H:i:s', $timestamp ),
476 'level' => $level,
477 'message' => $message,
478 'file' => $file,
479 'line' => $line_number,
480 'raw_line' => $line
481 );
482 }
483
484 return null;
485 }
486
487 /**
488 * Classify a log entry for relevance and priority.
489 *
490 * @since 1.8.9.2
491 *
492 * @param array $entry Parsed log entry.
493 * @return array|null
494 */
495 private function classify_log_entry( $entry ) {
496 $message = $entry['message'];
497 $file = $entry['file'] ?? '';
498 $level = $entry['level'] ?? '';
499
500 // Only process fatal errors and critical warnings
501 if ( ! preg_match( '/fatal error|parse error|warning/i', $level ) ) {
502 return null;
503 }
504
505 // Charitable-specific patterns (highest priority)
506 if ( $this->contains_charitable_patterns( $message, $file ) ) {
507 return array(
508 'priority' => 100,
509 'category' => 'charitable_error',
510 'description' => 'Direct Charitable plugin error'
511 );
512 }
513
514 // Payment gateway errors
515 if ( $this->contains_gateway_patterns( $message, $file ) ) {
516 return array(
517 'priority' => 80,
518 'category' => 'gateway_error',
519 'description' => 'Payment gateway related error'
520 );
521 }
522
523 // Donation context errors (during donation flow)
524 if ( $this->contains_donation_context( $message ) ) {
525 return array(
526 'priority' => 60,
527 'category' => 'donation_context',
528 'description' => 'Error occurred during donation process'
529 );
530 }
531
532 // Memory/resource errors
533 if ( preg_match( '/memory|maximum execution time|allowed memory size/i', $message ) ) {
534 return array(
535 'priority' => 40,
536 'category' => 'resource_error',
537 'description' => 'Server resource limitation error'
538 );
539 }
540
541 // Generic PHP fatal errors
542 if ( preg_match( '/fatal error|parse error/i', $level ) ) {
543 return array(
544 'priority' => 20,
545 'category' => 'php_fatal',
546 'description' => 'PHP fatal error'
547 );
548 }
549
550 return null;
551 }
552
553 /**
554 * Check if entry contains Charitable-specific patterns.
555 *
556 * @since 1.8.9.2
557 *
558 * @param string $message Error message.
559 * @param string $file File path.
560 * @return bool
561 */
562 private function contains_charitable_patterns( $message, $file ) {
563 $patterns = array(
564 '/charitable/i',
565 '/donation/i',
566 '/campaign/i',
567 '/wp-content\/plugins\/charitable/i'
568 );
569
570 $text_to_search = $message . ' ' . $file;
571
572 foreach ( $patterns as $pattern ) {
573 if ( preg_match( $pattern, $text_to_search ) ) {
574 return true;
575 }
576 }
577
578 return false;
579 }
580
581 /**
582 * Check if entry contains payment gateway patterns.
583 *
584 * @since 1.8.9.2
585 *
586 * @param string $message Error message.
587 * @param string $file File path.
588 * @return bool
589 */
590 private function contains_gateway_patterns( $message, $file ) {
591 $patterns = array(
592 '/stripe/i',
593 '/paypal/i',
594 '/square/i',
595 '/authorize\.net/i',
596 '/braintree/i',
597 '/payfast/i',
598 '/mollie/i',
599 '/payment.*gateway/i'
600 );
601
602 $text_to_search = $message . ' ' . $file;
603
604 foreach ( $patterns as $pattern ) {
605 if ( preg_match( $pattern, $text_to_search ) ) {
606 return true;
607 }
608 }
609
610 return false;
611 }
612
613 /**
614 * Check if entry contains donation context patterns.
615 *
616 * @since 1.8.9.2
617 *
618 * @param string $message Error message.
619 * @return bool
620 */
621 private function contains_donation_context( $message ) {
622 $patterns = array(
623 '/donation.*process/i',
624 '/checkout/i',
625 '/payment.*form/i',
626 '/donation.*form/i',
627 '/wp-json.*charitable/i',
628 '/charitable.*ajax/i'
629 );
630
631 foreach ( $patterns as $pattern ) {
632 if ( preg_match( $pattern, $message ) ) {
633 return true;
634 }
635 }
636
637 return false;
638 }
639
640 /**
641 * Generate error statistics.
642 *
643 * @since 1.8.9.2
644 *
645 * @param array $errors Array of errors.
646 * @return array
647 */
648 private function generate_error_statistics( $errors ) {
649 $stats = array(
650 'total_errors' => count( $errors ),
651 'by_category' => array(),
652 'by_priority' => array(),
653 'by_hour' => array(),
654 'most_common' => array()
655 );
656
657 foreach ( $errors as $error ) {
658 // Count by category
659 $category = $error['category'] ?? 'unknown';
660 $stats['by_category'][ $category ] = ( $stats['by_category'][ $category ] ?? 0 ) + 1;
661
662 // Count by priority
663 $priority = $error['priority'] ?? 0;
664 $priority_group = $this->get_priority_group( $priority );
665 $stats['by_priority'][ $priority_group ] = ( $stats['by_priority'][ $priority_group ] ?? 0 ) + 1;
666
667 // Count by hour
668 $hour = date( 'H:00', $error['timestamp'] );
669 $stats['by_hour'][ $hour ] = ( $stats['by_hour'][ $hour ] ?? 0 ) + 1;
670
671 // Track most common error messages (simplified)
672 $simplified_message = $this->simplify_error_message( $error['message'] );
673 $stats['most_common'][ $simplified_message ] = ( $stats['most_common'][ $simplified_message ] ?? 0 ) + 1;
674 }
675
676 // Sort most common errors
677 arsort( $stats['most_common'] );
678 $stats['most_common'] = array_slice( $stats['most_common'], 0, 5 );
679
680 return $stats;
681 }
682
683 /**
684 * Get priority group name.
685 *
686 * @since 1.8.9.2
687 *
688 * @param int $priority Priority score.
689 * @return string
690 */
691 private function get_priority_group( $priority ) {
692 if ( $priority >= 80 ) {
693 return 'Critical';
694 } elseif ( $priority >= 60 ) {
695 return 'High';
696 } elseif ( $priority >= 40 ) {
697 return 'Medium';
698 } else {
699 return 'Low';
700 }
701 }
702
703 /**
704 * Simplify error message for grouping.
705 *
706 * @since 1.8.9.2
707 *
708 * @param string $message Original error message.
709 * @return string
710 */
711 private function simplify_error_message( $message ) {
712 // Remove file paths and line numbers
713 $simplified = preg_replace( '/in \/[^\s]+ on line \d+/', '', $message );
714
715 // Remove specific values that might vary
716 $simplified = preg_replace( '/\b\d+\b/', '[NUMBER]', $simplified );
717
718 // Trim and limit length
719 $simplified = trim( $simplified );
720 if ( strlen( $simplified ) > 100 ) {
721 $simplified = substr( $simplified, 0, 100 ) . '...';
722 }
723
724 return $simplified;
725 }
726
727 /**
728 * Generate recommendations based on scan results.
729 *
730 * @since 1.8.9.2
731 *
732 * @param array $scan_results Full scan results.
733 * @return array
734 */
735 private function generate_recommendations( $scan_results ) {
736 $recommendations = array();
737
738 if ( empty( $scan_results['errors'] ) ) {
739 $recommendations[] = '�
740 No recent fatal errors found - your site appears to be running smoothly!';
741 return $recommendations;
742 }
743
744 $stats = $scan_results['statistics'];
745
746 // Charitable-specific errors
747 if ( isset( $stats['by_category']['charitable_error'] ) && $stats['by_category']['charitable_error'] > 0 ) {
748 $recommendations[] = '🔴 Charitable-specific errors detected. Please share these logs with support.';
749 }
750
751 // Gateway errors
752 if ( isset( $stats['by_category']['gateway_error'] ) && $stats['by_category']['gateway_error'] > 0 ) {
753 $recommendations[] = '💳 Payment gateway errors found. Check your payment processor settings.';
754 }
755
756 // Memory errors
757 if ( isset( $stats['by_category']['resource_error'] ) && $stats['by_category']['resource_error'] > 0 ) {
758 $recommendations[] = '⚠️ Memory/resource errors detected. Consider increasing PHP memory limit.';
759 }
760
761 // High error count
762 if ( $stats['total_errors'] > 10 ) {
763 $recommendations[] = '📈 High error frequency detected. Consider reviewing recent changes or plugin updates.';
764 }
765
766 // General advice
767 $recommendations[] = '💡 When sharing logs in forums, use the "Forum-Ready" format below to protect sensitive information.';
768
769 return $recommendations;
770 }
771
772 /**
773 * Get basic scan info for system info summary.
774 *
775 * @since 1.8.9.2
776 *
777 * @return array
778 */
779 private function get_basic_scan_info() {
780 $log_file = $this->get_debug_log_path();
781
782 $info = array(
783 'debug_enabled' => defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG,
784 'log_file_status' => $log_file ? 'Found' : 'Not found',
785 'file_size' => 'N/A',
786 'recent_fatals' => 0,
787 'charitable_errors' => 0,
788 'last_scan' => 'Never'
789 );
790
791 if ( $log_file && file_exists( $log_file ) ) {
792 $file_size_mb = round( filesize( $log_file ) / 1024 / 1024, 2 );
793 $info['file_size'] = $file_size_mb . ' MB';
794
795 // Quick scan for recent errors (lightweight version)
796 try {
797 $errors = $this->scan_recent_errors( 24 );
798 $info['recent_fatals'] = count( $errors );
799
800 $charitable_count = 0;
801 foreach ( $errors as $error ) {
802 if ( $error['category'] === 'charitable_error' ) {
803 $charitable_count++;
804 }
805 }
806 $info['charitable_errors'] = $charitable_count;
807
808 } catch ( Exception $e ) {
809 // Silent fail for basic info
810 }
811 }
812
813 // Check for cached results to show last scan time
814 $cached = get_transient( $this->cache_key );
815 if ( $cached && isset( $cached['timestamp'] ) ) {
816 $info['last_scan'] = $cached['timestamp'];
817 }
818
819 return $info;
820 }
821
822 /**
823 * Sanitize error entries for public sharing.
824 *
825 * @since 1.8.9.2
826 *
827 * @param array $errors Array of error entries.
828 * @return array
829 */
830 public function sanitize_errors_for_sharing( $errors ) {
831 $sanitized = array();
832
833 foreach ( $errors as $error ) {
834 $sanitized[] = array(
835 'datetime' => $error['datetime'],
836 'level' => $error['level'],
837 'category' => $error['category'],
838 'priority' => $error['priority'],
839 'description' => $error['description'],
840 'message' => $this->sanitize_error_message( $error['message'] ),
841 'file' => $this->sanitize_file_path( $error['file'] ?? '' ),
842 'line' => $error['line'] ?? ''
843 );
844 }
845
846 return $sanitized;
847 }
848
849 /**
850 * Sanitize error message for public sharing.
851 *
852 * @since 1.8.9.2
853 *
854 * @param string $message Original error message.
855 * @return string
856 */
857 private function sanitize_error_message( $message ) {
858 $patterns = array(
859 // Email addresses
860 '/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/' => '[EMAIL_REDACTED]',
861
862 // IP addresses
863 '/\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/' => '[IP_REDACTED]',
864
865 // Database credentials
866 '/DB_[A-Z]+[\'"][^\'"]*/' => 'DB_[REDACTED]',
867
868 // Common donor field patterns
869 '/first_name[\'"\s]*[=:][\'"\s]*[^\'",\s]+/i' => 'first_name=[REDACTED]',
870 '/last_name[\'"\s]*[=:][\'"\s]*[^\'",\s]+/i' => 'last_name=[REDACTED]',
871 '/donor_email[\'"\s]*[=:][\'"\s]*[^\'",\s]+/i' => 'donor_email=[REDACTED]',
872
873 // Credit card patterns
874 '/\b4[0-9]{12}(?:[0-9]{3})?\b/' => '[CARD_REDACTED]',
875 '/\b5[1-5][0-9]{14}\b/' => '[CARD_REDACTED]',
876
877 // Transaction IDs
878 '/(?:txn_|transaction_id|stripe_|paypal_)[a-zA-Z0-9_-]+/i' => '[TXN_REDACTED]',
879
880 // User IDs in contexts that might be sensitive
881 '/user_id[\'"\s]*[=:][\'"\s]*\d+/i' => 'user_id=[REDACTED]'
882 );
883
884 $sanitized = $message;
885 foreach ( $patterns as $pattern => $replacement ) {
886 $sanitized = preg_replace( $pattern, $replacement, $sanitized );
887 }
888
889 return $sanitized;
890 }
891
892 /**
893 * Sanitize file path for public sharing.
894 *
895 * @since 1.8.9.2
896 *
897 * @param string $file_path Original file path.
898 * @return string
899 */
900 private function sanitize_file_path( $file_path ) {
901 if ( empty( $file_path ) ) {
902 return '';
903 }
904
905 // Replace absolute paths with relative paths from WordPress root
906 $wp_root = ABSPATH;
907 $sanitized = str_replace( $wp_root, '/', $file_path );
908
909 // Replace common sensitive path components
910 $sanitized = preg_replace( '/\/home\/[^\/]+\//', '/home/[USER]/', $sanitized );
911 $sanitized = preg_replace( '/\/var\/www\/[^\/]+\//', '/var/www/[SITE]/', $sanitized );
912 $sanitized = preg_replace( '/\/Applications\/[^\/]+\//', '/Applications/[APP]/', $sanitized );
913
914 return $sanitized;
915 }
916 }
917
918 endif;