clear_report_events(); return; } $morning_time = ! empty( $options['morning_time'] ) ? $options['morning_time'] : '08:00'; $evening_time = ! empty( $options['evening_time'] ) ? $options['evening_time'] : '20:00'; $morning_timestamp = $this->calculate_next_timestamp( $morning_time ); $evening_timestamp = $this->calculate_next_timestamp( $evening_time ); if ( ! wp_next_scheduled( 'errorwatcher_morning_report' ) ) { wp_schedule_event( $morning_timestamp, 'daily', 'errorwatcher_morning_report' ); } if ( ! wp_next_scheduled( 'errorwatcher_evening_report' ) ) { wp_schedule_event( $evening_timestamp, 'daily', 'errorwatcher_evening_report' ); } } /** * Reschedule cron events (called after settings update). * * @return void */ public function reschedule_events(): void { $this->clear_report_events(); $this->schedule_events(); } /** * Clear scheduled report hooks. * * @return void */ public function clear_report_events(): void { wp_clear_scheduled_hook( 'errorwatcher_morning_report' ); wp_clear_scheduled_hook( 'errorwatcher_evening_report' ); } /** * Clear all plugin scheduled cron hooks (on deactivation). * * @return void */ public function clear_all_events(): void { $this->clear_report_events(); wp_clear_scheduled_hook( 'errorwatcher_cleanup_logs' ); } /** * Calculate the next UNIX timestamp for a given time string (HH:MM) in site timezone. * * @param string $time_str Time in 'HH:MM' 24-hour format. * @return int Next UNIX timestamp. */ private function calculate_next_timestamp( string $time_str ): int { $parts = explode( ':', $time_str ); $hours = isset( $parts[0] ) ? (int) $parts[0] : 8; $minutes = isset( $parts[1] ) ? (int) $parts[1] : 0; $timezone = wp_timezone(); $now = new \DateTime( 'now', $timezone ); $target = clone $now; $target->setTime( $hours, $minutes, 0 ); // If time for today has already passed, schedule for tomorrow. if ( $target->getTimestamp() <= $now->getTimestamp() ) { $target->modify( '+1 day' ); } return $target->getTimestamp(); } /** * Send morning email report. * * @return void */ public function send_morning_report( bool $force = false ): bool { return $this->dispatch_report( '', __( 'Morning Error Digest', 'errorwatcher' ), $force ); } /** * Send evening email report. * * @param bool $force Force dispatch even if reports toggle is off. * @return bool */ public function send_evening_report( bool $force = false ): bool { return $this->dispatch_report( '', __( 'Evening Error Digest', 'errorwatcher' ), $force ); } /** * Dispatch email report with statistics and attached .txt log. * * @param string $since_datetime MySQL datetime string. * @param string $report_label * @param bool $force Force dispatch. * @return bool */ public function dispatch_report( string $since_datetime, string $report_label, bool $force = false ): bool { $options = get_option( 'errorwatcher_settings', array() ); if ( ! $force && empty( $options['email_reports_enabled'] ) ) { return false; } $to_email_setting = ! empty( $options['notification_email'] ) ? $options['notification_email'] : get_option( 'admin_email' ); $recipients = array(); if ( is_string( $to_email_setting ) ) { foreach ( explode( ',', $to_email_setting ) as $single ) { $cleaned = sanitize_email( trim( $single ) ); if ( is_email( $cleaned ) ) { $recipients[] = $cleaned; } } } if ( empty( $recipients ) ) { $admin_email = get_option( 'admin_email' ); if ( is_email( $admin_email ) ) { $recipients[] = $admin_email; } } if ( empty( $recipients ) ) { return false; } $db = ErrorWatcher_DB::get_instance(); $stats = $db->get_statistics( $since_datetime ); $logs = $db->get_logs_since( $since_datetime, 1000 ); // If no errors were logged during this period, send a light summary or full report. $site_name = get_bloginfo( 'name' ); $site_url = site_url(); $time_format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' ); $period_to = wp_date( $time_format, current_time( 'timestamp' ) ); if ( ! empty( $since_datetime ) ) { $period_from = wp_date( $time_format, strtotime( $since_datetime ) ); } else { $period_from = __( 'All Active Errors', 'errorwatcher' ); } $subject = sprintf( '[%s] %s: %d Total Issues', $site_name, $report_label, $stats['total'] ); // Create temporary .txt log file for attachment. $attachment_path = $this->generate_txt_attachment( $logs, $report_label, $period_from, $period_to ); // Build HTML message. $message = $this->render_email_template( array( 'site_name' => $site_name, 'site_url' => $site_url, 'report_label' => $report_label, 'period_from' => $period_from, 'period_to' => $period_to, 'stats' => $stats, 'logs_count' => count( $logs ), 'logs' => array_slice( $logs, 0, 15 ), // Preview first 15 logs ) ); $headers = array( 'Content-Type: text/html; charset=UTF-8', sprintf( 'From: %s <%s>', esc_html( $site_name ), get_option( 'admin_email' ) ), ); $attachments = array(); if ( $attachment_path && file_exists( $attachment_path ) ) { $attachments[] = $attachment_path; } $sent = wp_mail( $recipients, $subject, $message, $headers, $attachments ); // Clean up temporary attachment file immediately. if ( $attachment_path && file_exists( $attachment_path ) ) { wp_delete_file( $attachment_path ); } return (bool) $sent; } /** * Send test email report. * * @param string $recipient Optional custom recipient. * @return array { success: bool, message: string } */ public function send_test_email( string $recipient = '' ): array { $options = get_option( 'errorwatcher_settings', array() ); if ( empty( $recipient ) ) { $recipient = ! empty( $options['notification_email'] ) ? $options['notification_email'] : get_option( 'admin_email' ); } $recipients = array(); if ( is_string( $recipient ) ) { foreach ( explode( ',', $recipient ) as $single ) { $cleaned = sanitize_email( trim( $single ) ); if ( is_email( $cleaned ) ) { $recipients[] = $cleaned; } } } if ( empty( $recipients ) ) { return array( 'success' => false, 'message' => __( 'Invalid recipient email address.', 'errorwatcher' ), ); } $db = ErrorWatcher_DB::get_instance(); $stats = $db->get_statistics(); $logs = $db->get_logs( array( 'per_page' => 10 ) )['items']; $site_name = get_bloginfo( 'name' ); $time_format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' ); $period_to = wp_date( $time_format, current_time( 'timestamp' ) ); $subject = sprintf( '[%s] Test Email Report - ErrorWatcher', $site_name ); $attachment_path = $this->generate_txt_attachment( $logs, 'Test Report', 'All Time', $period_to ); $message = $this->render_email_template( array( 'site_name' => $site_name, 'site_url' => site_url(), 'report_label' => __( 'Test Email Report', 'errorwatcher' ), 'period_from' => __( 'Test Period', 'errorwatcher' ), 'period_to' => $period_to, 'stats' => $stats, 'logs_count' => count( $logs ), 'logs' => $logs, 'is_test' => true, ) ); $headers = array( 'Content-Type: text/html; charset=UTF-8', sprintf( 'From: %s <%s>', esc_html( $site_name ), get_option( 'admin_email' ) ), ); $attachments = array(); if ( $attachment_path && file_exists( $attachment_path ) ) { $attachments[] = $attachment_path; } $sent = wp_mail( $recipients, $subject, $message, $headers, $attachments ); if ( $attachment_path && file_exists( $attachment_path ) ) { wp_delete_file( $attachment_path ); } if ( $sent ) { /* translators: %s: Recipient email address. */ $message = sprintf( __( 'Test email sent successfully to %s!', 'errorwatcher' ), esc_html( $recipient ) ); return array( 'success' => true, 'message' => $message, ); } return array( 'success' => false, 'message' => __( 'Failed to send test email. Please verify your WordPress mail/SMTP configuration.', 'errorwatcher' ), ); } /** * Generate a secure temporary .txt attachment file. * * @param array $logs * @param string $label * @param string $from * @param string $to * @return string|false Path to temporary file or false. */ private function generate_txt_attachment( array $logs, string $label, string $from, string $to ) { $upload_dir = wp_upload_dir(); $temp_dir = trailingslashit( $upload_dir['basedir'] ) . 'errorwatcher-temp'; if ( ! file_exists( $temp_dir ) ) { wp_mkdir_p( $temp_dir ); // Add an index.html and .htaccess to block direct access. @file_put_contents( $temp_dir . '/index.html', '' ); @file_put_contents( $temp_dir . '/.htaccess', "Order Deny,Allow\nDeny from all" ); } $filename = sprintf( 'error-logs-%s-%s.txt', sanitize_title( $label ), gmdate( 'Y-m-d-His' ) ); $filepath = $temp_dir . '/' . $filename; $content = "=======================================================\n"; $content .= " ERRORWATCHER - " . strtoupper( $label ) . "\n"; $content .= " Site: " . get_bloginfo( 'name' ) . " (" . site_url() . ")\n"; $content .= " Period: {$from} to {$to}\n"; $content .= " Total Records: " . count( $logs ) . "\n"; $content .= " Generated At: " . current_time( 'mysql' ) . "\n"; $content .= "=======================================================\n\n"; if ( empty( $logs ) ) { $content .= "No error logs recorded for this reporting period.\n"; } else { foreach ( $logs as $index => $log ) { $num = $index + 1; $content .= "-------------------------------------------------------\n"; $content .= "[#{$num}] {$log->error_type} (Severity: {$log->severity_code})\n"; $content .= "Occurrences: " . ( $log->occurrences ?? 1 ) . "\n"; $content .= "First Seen: {$log->created_at}\n"; $content .= "Last Seen: " . ( $log->last_seen ?? $log->created_at ) . "\n"; $content .= "File: {$log->file}:{$log->line}\n"; $content .= "Source: " . ErrorWatcher_DB::detect_source( $log->file ) . "\n"; $content .= "Request URL: " . ( $log->request_url ?: 'N/A' ) . " [{$log->http_method}]\n"; $content .= "Message: {$log->message}\n"; if ( ! empty( $log->stack_trace ) ) { $content .= "Stack Trace:\n" . $log->stack_trace . "\n"; } $content .= "\n"; } } $written = @file_put_contents( $filepath, $content ); return false !== $written ? $filepath : false; } /** * Build HTML email template without inline style tags to comply with WordPress.org guidelines. * * @param array $data * @return string HTML */ private function render_email_template( array $data ): string { $dashboard_url = admin_url( 'admin.php?page=errorwatcher' ); ob_start(); ?> <?php echo esc_html( $data['report_label'] ); ?>

• —

✓ Test Delivery Notice: This is a test email dispatched from your ErrorWatcher settings.

Below is the error activity digest for .

Fatal Errors Warnings Notices Deprecated

Recent Log Highlights

No errors recorded during this timeframe. Your site is running smoothly!

error_type ); if ( false !== strpos( $low_type, 'fatal' ) ) { $border_color = '#dc2626'; } elseif ( false !== strpos( $low_type, 'warning' ) ) { $border_color = '#d97706'; } elseif ( false !== strpos( $low_type, 'notice' ) ) { $border_color = '#2563eb'; } elseif ( false !== strpos( $low_type, 'deprecated' ) ) { $border_color = '#7c3aed'; } ?>
error_type ); ?>
message, 25 ) ); ?>
file ) ); ?>:line; ?> • created_at ); ?>

A complete detailed log file (.txt) is attached to this email.

Open ErrorWatcher Dashboard

Sent by ErrorWatcher on

prune_resolved_errors(); } }