PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.11
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.11
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-email-diagnostics.php

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

1,081 lines 32.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Tools - Email Diagnostics.
4 *
5 * @package Charitable/Classes/Charitable_Tools_Email_Diagnostics
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_Email_Diagnostics' ) ) :
18
19 /**
20 * Charitable_Tools_Email_Diagnostics
21 *
22 * @final
23 * @since 1.8.9.2
24 */
25 class Charitable_Tools_Email_Diagnostics {
26
27 /**
28 * The single instance of this class.
29 *
30 * @since 1.8.9.2
31 *
32 * @var Charitable_Tools_Email_Diagnostics|null
33 */
34 private static $instance = null;
35
36 /**
37 * Cache key for diagnostic results.
38 *
39 * @since 1.8.9.2
40 * @var string
41 */
42 private $cache_key = 'charitable_email_diagnostics_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 * Priority emails for core functionality testing.
54 *
55 * @since 1.8.9.2
56 * @var array
57 */
58 private $priority_emails = array(
59 'donation_receipt',
60 'offline_donation_receipt',
61 'new_donation'
62 );
63
64 /**
65 * Set up the class.
66 *
67 * @since 1.8.9.2
68 */
69 private function __construct() {
70 // Private constructor for singleton
71 }
72
73 /**
74 * Returns and/or create the single instance of this class.
75 *
76 * @since 1.8.9.2
77 *
78 * @return Charitable_Tools_Email_Diagnostics
79 */
80 public static function get_instance() {
81 if ( is_null( self::$instance ) ) {
82 self::$instance = new self();
83 }
84
85 return self::$instance;
86 }
87
88 /**
89 * Get email diagnostics summary for system info display.
90 *
91 * @since 1.8.9.2
92 *
93 * @return string
94 */
95 public function get_email_diagnostics_summary() {
96 $diagnostics = $this->get_cached_basic_diagnostics();
97
98 $data = "\n-- Email System Diagnostics\n\n";
99 $data .= sprintf( "Email Health Score: %s (%s)\n", $diagnostics['health_score'], $diagnostics['health_grade'] );
100 $data .= sprintf( "Emails Registered: %d emails\n", $diagnostics['emails_registered_count'] );
101 $data .= sprintf( "Email Access Status: %s\n", $diagnostics['email_access_status'] );
102 $data .= sprintf( "Preview URLs: %s functional\n", $diagnostics['preview_status'] );
103 $data .= sprintf( "Hook Timing: %s\n", $diagnostics['hook_timing'] );
104 $data .= sprintf( "Average Access Time: %s\n", $diagnostics['average_access_time'] );
105 $data .= sprintf( "Caching Detected: %s\n", $diagnostics['caching_detected'] );
106 $data .= sprintf( "Recent Email Errors: %d\n", $diagnostics['recent_errors'] );
107
108 return $data;
109 }
110
111 /**
112 * Get detailed email diagnostics for AJAX response.
113 *
114 * @since 1.8.9.2
115 *
116 * @return array
117 */
118 public function get_detailed_email_diagnostics() {
119 // Check for cached results first
120 $cached = get_transient( $this->cache_key );
121 if ( false !== $cached ) {
122 return $cached;
123 }
124
125 $diagnostics = array();
126 $start_time = microtime( true );
127
128 try {
129 // Run all diagnostic tests
130 $diagnostics['email_registration'] = $this->test_email_registration();
131 $diagnostics['email_access'] = $this->test_email_access();
132 $diagnostics['preview_functionality'] = $this->test_preview_functionality();
133 $diagnostics['hook_timing'] = $this->test_hook_timing();
134 $diagnostics['environment_config'] = $this->test_environment_config();
135 $diagnostics['performance'] = $this->test_performance();
136
137 // Calculate overall health score
138 $diagnostics['health_score'] = $this->calculate_health_score( $diagnostics );
139
140 // Add timing information
141 $diagnostics['execution_time'] = round( ( microtime( true ) - $start_time ) * 1000, 2 );
142 $diagnostics['timestamp'] = current_time( 'Y-m-d H:i:s' );
143
144 // Cache the results
145 set_transient( $this->cache_key, $diagnostics, $this->cache_duration );
146
147 } catch ( Exception $e ) {
148 $diagnostics['error'] = array(
149 'message' => 'Diagnostic test failed: ' . $e->getMessage(),
150 'code' => $e->getCode(),
151 'file' => $e->getFile(),
152 'line' => $e->getLine()
153 );
154 }
155
156 return $diagnostics;
157 }
158
159 /**
160 * Get cached basic diagnostics for system info.
161 *
162 * @since 1.8.9.2
163 *
164 * @return array
165 */
166 private function get_cached_basic_diagnostics() {
167 $cached = get_transient( $this->cache_key . '_basic' );
168 if ( false !== $cached ) {
169 return $cached;
170 }
171
172 try {
173 $emails_registered = $this->test_email_registration();
174 $email_access = $this->test_email_access();
175 $preview_test = $this->test_preview_functionality();
176 $hook_timing = $this->test_hook_timing();
177 $environment = $this->test_environment_config();
178
179 $basic = array(
180 'health_score' => $this->calculate_basic_health_score( $emails_registered, $email_access, $preview_test ),
181 'health_grade' => $this->get_health_grade( $this->calculate_basic_health_score( $emails_registered, $email_access, $preview_test ) ),
182 'emails_registered_count' => count( $emails_registered['registered_emails'] ?? array() ),
183 'email_access_status' => $email_access['status'] ?? 'Unknown',
184 'preview_status' => sprintf( '%d/%d', $preview_test['functional_count'] ?? 0, count( $this->priority_emails ) ),
185 'hook_timing' => $hook_timing['status'] ?? 'Unknown',
186 'average_access_time' => sprintf( '%.1fms', $email_access['average_time'] ?? 0 ),
187 'caching_detected' => implode( ', ', $environment['caching_plugins'] ?? array() ) ?: 'None',
188 'recent_errors' => $this->count_recent_email_errors()
189 );
190
191 set_transient( $this->cache_key . '_basic', $basic, $this->cache_duration );
192 return $basic;
193
194 } catch ( Exception $e ) {
195 return array(
196 'health_score' => '0/100',
197 'health_grade' => 'Grade: F',
198 'emails_registered_count' => 0,
199 'email_access_status' => 'Error',
200 'preview_status' => '0/' . count( $this->priority_emails ),
201 'hook_timing' => 'Error',
202 'average_access_time' => '0.0ms',
203 'caching_detected' => 'Unknown',
204 'recent_errors' => 0
205 );
206 }
207 }
208
209 /**
210 * Test email registration functionality.
211 *
212 * @since 1.8.9.2
213 *
214 * @return array
215 */
216 private function test_email_registration() {
217 $result = array(
218 'test_name' => 'Email Registration',
219 'status' => 'fail',
220 'score' => 0,
221 'max_score' => 20,
222 'details' => array(),
223 'registered_emails' => array(),
224 'timing' => array()
225 );
226
227 try {
228 $start_time = microtime( true );
229
230 // Test helper accessibility
231 $emails_helper = null;
232 try {
233 $emails_helper = charitable_get_helper( 'emails' );
234 $result['details']['helper_accessible'] = true;
235 } catch ( Exception $e ) {
236 $result['details']['helper_accessible'] = false;
237 $result['details']['helper_error'] = $e->getMessage();
238 }
239
240 if ( ! $emails_helper ) {
241 $result['details']['error'] = 'charitable_get_helper(\'emails\') returned null or false';
242 return $result;
243 }
244
245 // Test email registration via reflection
246 try {
247 $reflection = new ReflectionClass( $emails_helper );
248 $method = $reflection->getMethod( 'ensure_emails_registered' );
249 $registration_success = $method->invoke( $emails_helper );
250 $result['details']['registration_method_exists'] = true;
251 $result['details']['registration_successful'] = $registration_success;
252 } catch ( ReflectionException $e ) {
253 $result['details']['registration_method_exists'] = false;
254 $result['details']['reflection_error'] = $e->getMessage();
255 }
256
257 // Test getting available emails
258 $available_emails = $emails_helper->get_available_emails();
259 if ( is_array( $available_emails ) && ! empty( $available_emails ) ) {
260 $result['registered_emails'] = $available_emails;
261 $result['details']['emails_count'] = count( $available_emails );
262 $result['details']['has_priority_emails'] = $this->check_priority_emails_registered( $available_emails );
263
264 // Check if core emails are present
265 $core_emails_present = 0;
266 foreach ( $this->priority_emails as $email ) {
267 if ( isset( $available_emails[ $email ] ) ) {
268 $core_emails_present++;
269 }
270 }
271 $result['details']['core_emails_present'] = $core_emails_present;
272 $result['details']['core_emails_total'] = count( $this->priority_emails );
273
274 // Success if we have emails and core ones are present
275 if ( $core_emails_present >= 2 ) {
276 $result['status'] = 'success';
277 $result['score'] = 20;
278 } else {
279 $result['status'] = 'partial';
280 $result['score'] = 10;
281 }
282 } else {
283 $result['details']['emails_empty'] = true;
284 $result['details']['available_emails_result'] = $available_emails;
285 }
286
287 $end_time = microtime( true );
288 $result['timing'] = array(
289 'duration_ms' => round( ( $end_time - $start_time ) * 1000, 2 ),
290 'before_init' => ! did_action( 'init' ),
291 'during_init' => doing_action( 'init' ),
292 'after_init' => did_action( 'init' ) && ! doing_action( 'init' )
293 );
294
295 } catch ( Exception $e ) {
296 $result['details']['exception'] = $e->getMessage();
297 $result['details']['exception_trace'] = $e->getTraceAsString();
298 }
299
300 return $result;
301 }
302
303 /**
304 * Test email access functionality.
305 *
306 * @since 1.8.9.2
307 *
308 * @return array
309 */
310 private function test_email_access() {
311 $result = array(
312 'test_name' => 'Email Access',
313 'status' => 'fail',
314 'score' => 0,
315 'max_score' => 20,
316 'details' => array(),
317 'email_tests' => array(),
318 'average_time' => 0
319 );
320
321 try {
322 $emails_helper = charitable_get_helper( 'emails' );
323 if ( ! $emails_helper ) {
324 $result['details']['error'] = 'Email helper not available';
325 return $result;
326 }
327
328 $total_time = 0;
329 $successful_accesses = 0;
330 $failed_accesses = 0;
331
332 foreach ( $this->priority_emails as $email_id ) {
333 $email_test = array(
334 'email_id' => $email_id,
335 'accessible' => false,
336 'class_exists' => false,
337 'instantiable' => false,
338 'timing_ms' => 0
339 );
340
341 $start_time = microtime( true );
342
343 try {
344 // Test getting email class
345 $email_class = $emails_helper->get_email( $email_id );
346 $email_test['class_name'] = $email_class;
347 $email_test['class_retrieved'] = ! empty( $email_class );
348
349 if ( $email_class && class_exists( $email_class ) ) {
350 $email_test['class_exists'] = true;
351
352 // Test instantiation
353 try {
354 $email_instance = new $email_class();
355 $email_test['instantiable'] = true;
356 $email_test['accessible'] = true;
357
358 // Test basic methods
359 if ( method_exists( $email_instance, 'get_email_id' ) ) {
360 $email_test['get_id_works'] = $email_instance->get_email_id() === $email_id;
361 }
362 if ( method_exists( $email_instance, 'get_name' ) ) {
363 $email_test['has_name'] = ! empty( $email_instance->get_name() );
364 }
365
366 // Phase 1 Integration: Test recipient extraction with real donation data
367 $email_test['recipient_extraction'] = $this->test_recipient_extraction_for_email( $email_instance, $email_id );
368
369 $successful_accesses++;
370 } catch ( Exception $e ) {
371 $email_test['instantiation_error'] = $e->getMessage();
372 $failed_accesses++;
373 }
374 } else {
375 $email_test['class_missing'] = true;
376 $failed_accesses++;
377 }
378
379 } catch ( Exception $e ) {
380 $email_test['access_error'] = $e->getMessage();
381 $failed_accesses++;
382 }
383
384 $end_time = microtime( true );
385 $email_test['timing_ms'] = round( ( $end_time - $start_time ) * 1000, 2 );
386 $total_time += $email_test['timing_ms'];
387
388 $result['email_tests'][ $email_id ] = $email_test;
389 }
390
391 // Calculate results
392 $result['details']['successful_accesses'] = $successful_accesses;
393 $result['details']['failed_accesses'] = $failed_accesses;
394 $result['details']['total_emails_tested'] = count( $this->priority_emails );
395 $result['average_time'] = round( $total_time / count( $this->priority_emails ), 1 );
396
397 // Phase 1 Integration: Count recipient extraction failures
398 $recipient_failures = 0;
399 foreach ( $result['email_tests'] as $email_test ) {
400 if ( isset( $email_test['recipient_extraction']['tested'] ) &&
401 $email_test['recipient_extraction']['tested'] &&
402 ! $email_test['recipient_extraction']['success'] ) {
403 $recipient_failures++;
404 }
405 }
406 $result['details']['recipient_extraction_failures'] = $recipient_failures;
407
408 // Determine status and score
409 if ( $successful_accesses === count( $this->priority_emails ) ) {
410 $result['status'] = 'success';
411 $result['score'] = 20;
412
413 // Reduce score for recipient extraction failures
414 if ( $recipient_failures > 0 ) {
415 $result['score'] -= ( $recipient_failures * 3 ); // -3 points per failure
416 $result['status'] = 'partial';
417 $result['details']['recipient_warning'] = "Recipient extraction failing for {$recipient_failures} email(s)";
418 }
419 } elseif ( $successful_accesses > 0 ) {
420 $result['status'] = 'partial';
421 $result['score'] = round( ( $successful_accesses / count( $this->priority_emails ) ) * 20 );
422
423 // Additional penalty for recipient failures
424 if ( $recipient_failures > 0 ) {
425 $result['score'] -= ( $recipient_failures * 2 );
426 }
427 }
428
429 // Performance check
430 if ( $result['average_time'] > 100 ) {
431 $result['details']['performance_warning'] = 'Average access time exceeds 100ms';
432 $result['score'] = max( 0, $result['score'] - 5 ); // Penalty for slow access
433 }
434
435 } catch ( Exception $e ) {
436 $result['details']['exception'] = $e->getMessage();
437 }
438
439 return $result;
440 }
441
442 /**
443 * Test email preview functionality.
444 *
445 * @since 1.8.9.2
446 *
447 * @return array
448 */
449 private function test_preview_functionality() {
450 $result = array(
451 'test_name' => 'Preview Functionality',
452 'status' => 'fail',
453 'score' => 0,
454 'max_score' => 15,
455 'details' => array(),
456 'preview_tests' => array(),
457 'functional_count' => 0
458 );
459
460 try {
461 $functional_previews = 0;
462
463 foreach ( $this->priority_emails as $email_id ) {
464 $preview_test = array(
465 'email_id' => $email_id,
466 'url_generated' => false,
467 'endpoint_accessible' => false,
468 'preview_works' => false
469 );
470
471 try {
472 // Generate preview URL
473 $preview_url = add_query_arg(
474 array(
475 'charitable_action' => 'preview_email',
476 'email_id' => $email_id,
477 '_nonce' => wp_create_nonce( 'email_preview' )
478 ),
479 site_url()
480 );
481
482 $preview_test['url_generated'] = true;
483 $preview_test['preview_url'] = $preview_url;
484
485 // Test endpoint accessibility (HEAD request only - no actual email sending)
486 $response = wp_remote_head( $preview_url, array(
487 'timeout' => 10,
488 'user-agent' => 'Charitable Email Diagnostics'
489 ) );
490
491 if ( ! is_wp_error( $response ) ) {
492 $status_code = wp_remote_retrieve_response_code( $response );
493 $preview_test['status_code'] = $status_code;
494 $preview_test['endpoint_accessible'] = true;
495
496 // Check for successful response or redirect (both are acceptable for previews)
497 if ( in_array( $status_code, array( 200, 301, 302 ) ) ) {
498 $preview_test['preview_works'] = true;
499 $functional_previews++;
500 }
501 } else {
502 $preview_test['endpoint_error'] = $response->get_error_message();
503 }
504
505 } catch ( Exception $e ) {
506 $preview_test['exception'] = $e->getMessage();
507 }
508
509 $result['preview_tests'][ $email_id ] = $preview_test;
510 }
511
512 $result['functional_count'] = $functional_previews;
513 $result['details']['total_tested'] = count( $this->priority_emails );
514
515 // Determine status and score
516 if ( $functional_previews === count( $this->priority_emails ) ) {
517 $result['status'] = 'success';
518 $result['score'] = 15;
519 } elseif ( $functional_previews > 0 ) {
520 $result['status'] = 'partial';
521 $result['score'] = round( ( $functional_previews / count( $this->priority_emails ) ) * 15 );
522 }
523
524 } catch ( Exception $e ) {
525 $result['details']['exception'] = $e->getMessage();
526 }
527
528 return $result;
529 }
530
531 /**
532 * Test hook timing and initialization order.
533 *
534 * @since 1.8.9.2
535 *
536 * @return array
537 */
538 private function test_hook_timing() {
539 $result = array(
540 'test_name' => 'Hook Timing',
541 'status' => 'success',
542 'score' => 15,
543 'max_score' => 15,
544 'details' => array()
545 );
546
547 try {
548 // Check WordPress hook status
549 $result['details']['init_completed'] = did_action( 'init' );
550 $result['details']['admin_init_completed'] = did_action( 'admin_init' );
551 $result['details']['currently_doing_init'] = doing_action( 'init' );
552 $result['details']['currently_doing_admin_init'] = doing_action( 'admin_init' );
553
554 // Check current action context
555 $current_action = current_action();
556 $result['details']['current_action'] = $current_action ?: 'none';
557
558 // Check plugin load order
559 $active_plugins = get_option( 'active_plugins', array() );
560 $charitable_position = false;
561 foreach ( $active_plugins as $index => $plugin ) {
562 if ( strpos( $plugin, 'charitable' ) !== false ) {
563 $charitable_position = $index;
564 break;
565 }
566 }
567 $result['details']['charitable_plugin_position'] = $charitable_position;
568 $result['details']['total_active_plugins'] = count( $active_plugins );
569
570 // Check for timing conflicts
571 $timing_issues = array();
572
573 if ( ! did_action( 'init' ) && ! doing_action( 'init' ) ) {
574 $timing_issues[] = 'Emails accessed before init hook';
575 $result['score'] -= 5;
576 }
577
578 if ( doing_action( 'plugins_loaded' ) ) {
579 $timing_issues[] = 'Accessing during plugins_loaded (may cause timing issues)';
580 $result['score'] -= 3;
581 }
582
583 // Check for AJAX context
584 if ( wp_doing_ajax() ) {
585 $result['details']['ajax_context'] = true;
586 // AJAX is actually fine for email diagnostics
587 }
588
589 // Check for cron context
590 if ( wp_doing_cron() ) {
591 $result['details']['cron_context'] = true;
592 $timing_issues[] = 'Running in cron context (may have limited functionality)';
593 $result['score'] -= 2;
594 }
595
596 $result['details']['timing_issues'] = $timing_issues;
597
598 // Overall status
599 if ( empty( $timing_issues ) ) {
600 $result['status'] = 'success';
601 } elseif ( $result['score'] > 10 ) {
602 $result['status'] = 'warning';
603 } else {
604 $result['status'] = 'fail';
605 }
606
607 } catch ( Exception $e ) {
608 $result['details']['exception'] = $e->getMessage();
609 $result['status'] = 'fail';
610 $result['score'] = 0;
611 }
612
613 return $result;
614 }
615
616 /**
617 * Test environment configuration.
618 *
619 * @since 1.8.9.2
620 *
621 * @return array
622 */
623 private function test_environment_config() {
624 $result = array(
625 'test_name' => 'Environment Configuration',
626 'status' => 'success',
627 'score' => 15,
628 'max_score' => 15,
629 'details' => array(),
630 'caching_plugins' => array(),
631 'environment_info' => array()
632 );
633
634 try {
635 // Detect caching plugins
636 $known_caching_plugins = array(
637 'wp-rocket/wp-rocket.php' => 'WP Rocket',
638 'w3-total-cache/w3-total-cache.php' => 'W3 Total Cache',
639 'wp-super-cache/wp-cache.php' => 'WP Super Cache',
640 'litespeed-cache/litespeed-cache.php' => 'LiteSpeed Cache',
641 'wp-fastest-cache/wpFastestCache.php' => 'WP Fastest Cache',
642 'hummingbird-performance/wp-hummingbird.php' => 'Hummingbird',
643 'autoptimize/autoptimize.php' => 'Autoptimize'
644 );
645
646 $active_plugins = get_option( 'active_plugins', array() );
647 foreach ( $known_caching_plugins as $plugin_file => $plugin_name ) {
648 if ( in_array( $plugin_file, $active_plugins ) ) {
649 $result['caching_plugins'][] = $plugin_name;
650 }
651 }
652
653 // Check object cache
654 $result['details']['object_cache_enabled'] = wp_using_ext_object_cache();
655
656 // Check OPcache
657 $result['details']['opcache_enabled'] = function_exists( 'opcache_get_status' ) && opcache_get_status();
658
659 // Environment information
660 $result['environment_info'] = array(
661 'php_version' => PHP_VERSION,
662 'wordpress_version' => get_bloginfo( 'version' ),
663 'charitable_version' => charitable()->get_version(),
664 'memory_limit' => ini_get( 'memory_limit' ),
665 'max_execution_time' => ini_get( 'max_execution_time' ),
666 'is_multisite' => is_multisite(),
667 'debug_enabled' => defined( 'WP_DEBUG' ) && WP_DEBUG,
668 'charitable_debug' => defined( 'CHARITABLE_DEBUG' ) && CHARITABLE_DEBUG
669 );
670
671 // Check email environment
672 $result['details']['smtp_configured'] = $this->is_smtp_configured();
673 $result['details']['mail_function_available'] = function_exists( 'mail' );
674 $result['details']['wp_mail_available'] = function_exists( 'wp_mail' );
675
676 // Score adjustments
677 if ( ! empty( $result['caching_plugins'] ) ) {
678 $result['details']['caching_warning'] = 'Caching plugins detected - may affect email timing';
679 $result['score'] -= 2;
680 }
681
682 if ( ! $result['details']['wp_mail_available'] ) {
683 $result['details']['mail_error'] = 'wp_mail function not available';
684 $result['score'] -= 5;
685 $result['status'] = 'warning';
686 }
687
688 } catch ( Exception $e ) {
689 $result['details']['exception'] = $e->getMessage();
690 $result['status'] = 'fail';
691 $result['score'] = 0;
692 }
693
694 return $result;
695 }
696
697 /**
698 * Test performance metrics.
699 *
700 * @since 1.8.9.2
701 *
702 * @return array
703 */
704 private function test_performance() {
705 $result = array(
706 'test_name' => 'Performance',
707 'status' => 'success',
708 'score' => 15,
709 'max_score' => 15,
710 'details' => array(),
711 'metrics' => array()
712 );
713
714 try {
715 $start_time = microtime( true );
716 $start_memory = memory_get_usage( true );
717
718 // Test email helper performance
719 $helper_start = microtime( true );
720 $emails_helper = charitable_get_helper( 'emails' );
721 $helper_time = ( microtime( true ) - $helper_start ) * 1000;
722
723 // Test email registration performance
724 $reg_start = microtime( true );
725 if ( $emails_helper ) {
726 $available_emails = $emails_helper->get_available_emails();
727 $reg_time = ( microtime( true ) - $reg_start ) * 1000;
728 } else {
729 $reg_time = 0;
730 $result['details']['helper_unavailable'] = true;
731 }
732
733 // Calculate metrics
734 $total_time = ( microtime( true ) - $start_time ) * 1000;
735 $memory_used = memory_get_usage( true ) - $start_memory;
736
737 $result['metrics'] = array(
738 'helper_access_time_ms' => round( $helper_time, 2 ),
739 'registration_time_ms' => round( $reg_time, 2 ),
740 'total_time_ms' => round( $total_time, 2 ),
741 'memory_used_kb' => round( $memory_used / 1024, 2 ),
742 'peak_memory_mb' => round( memory_get_peak_usage( true ) / 1024 / 1024, 2 )
743 );
744
745 // Performance scoring
746 if ( $helper_time > 50 ) {
747 $result['details']['slow_helper'] = 'Email helper access > 50ms';
748 $result['score'] -= 3;
749 }
750
751 if ( $reg_time > 100 ) {
752 $result['details']['slow_registration'] = 'Email registration > 100ms';
753 $result['score'] -= 5;
754 }
755
756 if ( $memory_used > 1024 * 1024 ) { // 1MB
757 $result['details']['high_memory'] = 'High memory usage during email operations';
758 $result['score'] -= 2;
759 }
760
761 // Overall status
762 if ( $result['score'] >= 13 ) {
763 $result['status'] = 'excellent';
764 } elseif ( $result['score'] >= 10 ) {
765 $result['status'] = 'good';
766 } elseif ( $result['score'] >= 7 ) {
767 $result['status'] = 'fair';
768 } else {
769 $result['status'] = 'poor';
770 }
771
772 } catch ( Exception $e ) {
773 $result['details']['exception'] = $e->getMessage();
774 $result['status'] = 'fail';
775 $result['score'] = 0;
776 }
777
778 return $result;
779 }
780
781 /**
782 * Calculate overall health score from diagnostic results.
783 *
784 * @since 1.8.9.2
785 *
786 * @param array $diagnostics Full diagnostic results.
787 * @return array Health score details.
788 */
789 private function calculate_health_score( $diagnostics ) {
790 $total_score = 0;
791 $max_score = 100;
792 $category_scores = array();
793
794 // Extract scores from each category
795 $categories = array(
796 'email_registration',
797 'email_access',
798 'preview_functionality',
799 'hook_timing',
800 'environment_config',
801 'performance'
802 );
803
804 foreach ( $categories as $category ) {
805 if ( isset( $diagnostics[ $category ]['score'] ) ) {
806 $score = (int) $diagnostics[ $category ]['score'];
807 $max = (int) $diagnostics[ $category ]['max_score'];
808 $total_score += $score;
809 $category_scores[ $category ] = array(
810 'score' => $score,
811 'max_score' => $max,
812 'percentage' => $max > 0 ? round( ( $score / $max ) * 100 ) : 0
813 );
814 }
815 }
816
817 $percentage = round( ( $total_score / $max_score ) * 100 );
818 $grade = $this->get_health_grade( $percentage );
819
820 return array(
821 'total_score' => $total_score,
822 'max_score' => $max_score,
823 'percentage' => $percentage,
824 'grade' => $grade,
825 'category_scores' => $category_scores,
826 'interpretation' => $this->get_score_interpretation( $percentage )
827 );
828 }
829
830 /**
831 * Calculate basic health score for system info display.
832 *
833 * @since 1.8.9.2
834 *
835 * @param array $email_registration Email registration test results.
836 * @param array $email_access Email access test results.
837 * @param array $preview_test Preview functionality test results.
838 * @return int Health score percentage.
839 */
840 private function calculate_basic_health_score( $email_registration, $email_access, $preview_test ) {
841 $total_score = 0;
842 $max_score = 55; // Registration (20) + Access (20) + Preview (15)
843
844 $total_score += $email_registration['score'] ?? 0;
845 $total_score += $email_access['score'] ?? 0;
846 $total_score += $preview_test['score'] ?? 0;
847
848 return round( ( $total_score / $max_score ) * 100 );
849 }
850
851 /**
852 * Get health grade based on percentage.
853 *
854 * @since 1.8.9.2
855 *
856 * @param int $percentage Health score percentage.
857 * @return string Grade letter with descriptive text.
858 */
859 private function get_health_grade( $percentage ) {
860 if ( $percentage >= 90 ) {
861 return 'Grade: A';
862 } elseif ( $percentage >= 80 ) {
863 return 'Grade: B';
864 } elseif ( $percentage >= 70 ) {
865 return 'Grade: C';
866 } elseif ( $percentage >= 60 ) {
867 return 'Grade: D';
868 } else {
869 return 'Grade: F';
870 }
871 }
872
873 /**
874 * Get score interpretation and recommendations.
875 *
876 * @since 1.8.9.2
877 *
878 * @param int $percentage Health score percentage.
879 * @return string Interpretation text.
880 */
881 private function get_score_interpretation( $percentage ) {
882 if ( $percentage >= 90 ) {
883 return 'Excellent: Email system is functioning optimally with no issues detected.';
884 } elseif ( $percentage >= 80 ) {
885 return 'Good: Email system is working well with minor issues that do not affect core functionality.';
886 } elseif ( $percentage >= 70 ) {
887 return 'Fair: Email system has some issues that may affect performance or reliability.';
888 } elseif ( $percentage >= 60 ) {
889 return 'Poor: Email system has significant issues that need attention.';
890 } else {
891 return 'Critical: Email system has major problems that require immediate investigation.';
892 }
893 }
894
895 /**
896 * Check if priority emails are registered.
897 *
898 * @since 1.8.9.2
899 *
900 * @param array $available_emails Available emails array.
901 * @return bool True if priority emails are registered.
902 */
903 private function check_priority_emails_registered( $available_emails ) {
904 foreach ( $this->priority_emails as $email ) {
905 if ( ! isset( $available_emails[ $email ] ) ) {
906 return false;
907 }
908 }
909 return true;
910 }
911
912 /**
913 * Check if SMTP is configured.
914 *
915 * @since 1.8.9.2
916 *
917 * @return bool True if SMTP appears to be configured.
918 */
919 private function is_smtp_configured() {
920 // Check for common SMTP plugins
921 $smtp_plugins = array(
922 'wp-mail-smtp/wp_mail_smtp.php',
923 'easy-wp-smtp/easy-wp-smtp.php',
924 'post-smtp/postman-smtp.php',
925 'wp-smtp/wp-smtp.php'
926 );
927
928 $active_plugins = get_option( 'active_plugins', array() );
929 foreach ( $smtp_plugins as $plugin ) {
930 if ( in_array( $plugin, $active_plugins ) ) {
931 return true;
932 }
933 }
934
935 // Check for manual SMTP configuration
936 if ( defined( 'WPMS_ON' ) && WPMS_ON ) {
937 return true;
938 }
939
940 return false;
941 }
942
943 /**
944 * Count recent email errors from logs.
945 *
946 * @since 1.8.9.2
947 * @since 1.8.9.5 Enhanced to connect with Phase 1 error logging improvements.
948 *
949 * @return int Number of recent email errors.
950 */
951 private function count_recent_email_errors() {
952 $error_count = 0;
953
954 try {
955 // Connect to Phase 1 enhanced error logging via activities table (Pro only)
956 $registered_tables = (array) apply_filters( 'charitable_db_tables', array() );
957 if ( function_exists( 'charitable_get_table' ) && array_key_exists( 'charitable_activities', $registered_tables ) ) {
958 $activities_table = charitable_get_table( 'charitable_activities' );
959 if ( $activities_table ) {
960 global $wpdb;
961 $table_name = $activities_table->get_table_name();
962 $seven_days_ago = date( 'Y-m-d H:i:s', strtotime( '-7 days' ) );
963
964 // Count email failures from the last 7 days
965 $error_count = (int) $wpdb->get_var( $wpdb->prepare(
966 "SELECT COUNT(*) FROM {$table_name}
967 WHERE type = 'form_error'
968 AND error_type = 'email_failure'
969 AND timestamp >= %s",
970 $seven_days_ago
971 ) );
972 }
973 }
974 } catch ( Exception $e ) {
975 // Fail silently for diagnostics - don't break the diagnostic system
976 if ( charitable_is_debug() ) {
977 error_log( 'Charitable Email Diagnostics: Error counting recent email errors - ' . $e->getMessage() );
978 }
979 }
980
981 return $error_count;
982 }
983
984 /**
985 * Test recipient extraction for a specific email with real donation data.
986 *
987 * @since 1.8.9.5 Phase 1 Integration
988 * @since 1.8.10 Handle array recipients to prevent fatal TypeError in is_email().
989 *
990 * @param object $email_instance Email instance to test.
991 * @param string $email_id Email ID being tested.
992 * @return array Test results.
993 */
994 private function test_recipient_extraction_for_email( $email_instance, $email_id ) {
995 $extraction_test = array(
996 'tested' => false,
997 'success' => false,
998 'recipient_valid' => false,
999 'recipient_value' => '',
1000 'donation_context' => 'none',
1001 'error' => null
1002 );
1003
1004 try {
1005 // Skip recipient extraction test for emails that don't need donations
1006 $non_donation_emails = array( 'password_reset', 'email_verification' );
1007 if ( in_array( $email_id, $non_donation_emails ) ) {
1008 $extraction_test['tested'] = true;
1009 $extraction_test['success'] = true;
1010 $extraction_test['recipient_value'] = 'N/A - No donation required';
1011 return $extraction_test;
1012 }
1013
1014 // Find a recent completed donation for testing
1015 $recent_donations = get_posts( array(
1016 'post_type' => 'donation',
1017 'post_status' => 'charitable-completed',
1018 'posts_per_page' => 1,
1019 'orderby' => 'date',
1020 'order' => 'DESC'
1021 ) );
1022
1023 if ( empty( $recent_donations ) ) {
1024 $extraction_test['donation_context'] = 'no_donations';
1025 $extraction_test['error'] = 'No completed donations available for testing';
1026 return $extraction_test;
1027 }
1028
1029 $donation = charitable_get_donation( $recent_donations[0]->ID );
1030 if ( ! $donation ) {
1031 $extraction_test['donation_context'] = 'invalid_donation';
1032 $extraction_test['error'] = 'Could not load donation object';
1033 return $extraction_test;
1034 }
1035
1036 $extraction_test['donation_context'] = 'donation_' . $donation->get_donation_id();
1037 $extraction_test['tested'] = true;
1038
1039 // Create new email instance with donation context
1040 $email_class = get_class( $email_instance );
1041 $test_email = new $email_class( array( 'donation' => $donation ) );
1042
1043 // Test recipient extraction
1044 if ( method_exists( $test_email, 'get_recipient' ) ) {
1045 $recipient = $test_email->get_recipient();
1046 $extraction_test['recipient_value'] = $recipient;
1047 if ( is_array( $recipient ) ) {
1048 $extraction_test['recipient_valid'] = ! empty( $recipient ) && ( array_filter( $recipient, 'is_email' ) === $recipient );
1049 $extraction_test['success'] = ! empty( $recipient );
1050 } else {
1051 $extraction_test['recipient_valid'] = is_email( $recipient );
1052 $extraction_test['success'] = ! empty( $recipient ) && is_email( $recipient );
1053 }
1054 } else {
1055 $extraction_test['error'] = 'get_recipient method not available';
1056 }
1057
1058 } catch ( Exception $e ) {
1059 $extraction_test['error'] = $e->getMessage();
1060 $extraction_test['exception_file'] = basename( $e->getFile() );
1061 $extraction_test['exception_line'] = $e->getLine();
1062 }
1063
1064 return $extraction_test;
1065 }
1066
1067 /**
1068 * Clear diagnostic cache.
1069 *
1070 * @since 1.8.9.2
1071 *
1072 * @return bool True if cache was cleared.
1073 */
1074 public function clear_cache() {
1075 $result1 = delete_transient( $this->cache_key );
1076 $result2 = delete_transient( $this->cache_key . '_basic' );
1077 return $result1 || $result2;
1078 }
1079 }
1080
1081 endif;