604800, // 7 days in seconds 'display' => __('Once Weekly', 'activity-log-pro') ); $schedules['monthly'] = array( 'interval' => 2629746, // 30.44 days in seconds (average month) 'display' => __('Once Monthly', 'activity-log-pro') ); return $schedules; } /** * Register email digest settings with WordPress Settings API. * * Registers all necessary options for email digest functionality * including frequency, recipients, timing, and feature toggles. * * @return void * @since 1.1.0 */ public static function register_settings() { // Settings are now registered in the ACTLOGPRO_Admin class to ensure proper timing // This ensures they're registered at the same time as all other plugin settings } /** * Sanitize email digest frequency selection. * * Validates and sanitizes the frequency setting to ensure only * allowed values (daily, weekly, monthly) are stored. * * @param string $frequency Raw frequency value from user input. * @return string Sanitized frequency value. * @since 1.1.0 */ public static function sanitize_frequency($frequency) { $allowed_frequencies = array('daily', 'weekly', 'monthly'); return in_array($frequency, $allowed_frequencies, true) ? $frequency : 'weekly'; } /** * Sanitize day selection for digest scheduling. * * Validates day selection based on frequency type. For weekly reports, * allows weekday names. For monthly reports, allows day numbers 1-28. * * @param string $day Raw day value from user input. * @return string Sanitized day value. * @since 1.1.0 */ public static function sanitize_day($day) { $frequency = get_option('actlogpro_email_digest_frequency', 'weekly'); if ($frequency === 'weekly') { $allowed_days = array('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'); return in_array(strtolower($day), $allowed_days, true) ? strtolower($day) : 'monday'; } elseif ($frequency === 'monthly') { $day_number = (int) $day; return ($day_number >= 1 && $day_number <= 28) ? (string) $day_number : '1'; } return 'monday'; // Default fallback } /** * Sanitize time setting for digest delivery. * * Validates and sanitizes time input to HH:MM format, * ensuring valid 24-hour time format. * * @param string $time Raw time value from user input. * @return string Sanitized time in HH:MM format. * @since 1.1.0 */ public static function sanitize_time($time) { // Remove any non-digit characters except colon $time = preg_replace('/[^0-9:]/', '', $time); // Validate time format if (preg_match('/^([01]?[0-9]|2[0-3]):([0-5][0-9])$/', $time, $matches)) { return sprintf('%02d:%02d', $matches[1], $matches[2]); } return '08:00'; // Default to 8:00 AM if invalid } /** * Sanitize and validate email recipients list. * * Processes textarea input containing email addresses separated by * newlines, spaces, or commas. Validates each email using WordPress * is_email() function and returns clean newline-separated list. * * @param string $recipients Raw recipients textarea content. * @return string Sanitized email list, one per line. * @since 1.1.0 */ public static function sanitize_email_recipients($recipients) { // Sanitize textarea input $recipients = sanitize_textarea_field($recipients); // Split by various delimiters (newlines, commas, spaces) $email_list = preg_split('/[\s,\n\r]+/', $recipients, -1, PREG_SPLIT_NO_EMPTY); // Validate and filter email addresses $valid_emails = array_filter($email_list, 'is_email'); // Remove duplicates and reindex $valid_emails = array_values(array_unique($valid_emails)); // Return as newline-separated string return implode("\n", $valid_emails); } /** * Register REST API endpoints for digest preview and testing. * * Creates endpoints for HTML preview display and test email sending * functionality with proper permission callbacks and nonce verification. * * @return void * @since 1.1.0 */ public static function register_rest_endpoints() { // HTML preview endpoint for immediate popup preview register_rest_route( self::REST_NAMESPACE, '/email-digest/preview/html', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array(__CLASS__, 'rest_preview_html'), 'permission_callback' => array(__CLASS__, 'rest_permission_check'), 'args' => array( '_wpnonce' => array( 'required' => true, 'validate_callback' => array(__CLASS__, 'validate_nonce') ) ) ) ); // Test email sending endpoint register_rest_route( self::REST_NAMESPACE, '/email-digest/preview/send-test', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array(__CLASS__, 'rest_send_test'), 'permission_callback' => array(__CLASS__, 'rest_permission_check'), 'args' => array( '_wpnonce' => array( 'required' => true, 'validate_callback' => array(__CLASS__, 'validate_nonce') ) ) ) ); } /** * Validate nonce for REST API security. * * Validates WordPress nonce to ensure requests are legitimate * and prevent CSRF attacks on email digest endpoints. * * @param string $nonce Nonce value from request. * @return bool True if nonce is valid, false otherwise. * @since 1.1.0 */ public static function validate_nonce($nonce) { return wp_verify_nonce($nonce, 'wp_rest'); } /** * Permission callback for REST API endpoints. * * Ensures only administrators can access email digest preview * and test functionality for security purposes. * * @return bool True if user has permission, false otherwise. * @since 1.1.0 */ public static function rest_permission_check() { return current_user_can('manage_options'); } /** * REST API endpoint callback for HTML email preview. * * Generates and returns HTML preview of email digest report * for immediate display in admin popup window. * * @param WP_REST_Request $request REST request object. * @return WP_REST_Response|WP_Error Response with HTML content or error. * @since 1.1.0 */ public static function rest_preview_html($request) { try { // Get sample data for preview $sample_data = self::get_digest_data(true); // Generate HTML email content $html_content = self::generate_email_html($sample_data); // Return HTML response with proper headers for iframe display $response = new WP_REST_Response($html_content); $response->header('Content-Type', 'text/html; charset=UTF-8'); $response->header('Cache-Control', 'no-cache, must-revalidate, max-age=0'); $response->header('X-Content-Type-Options', 'nosniff'); $response->header('X-Frame-Options', 'SAMEORIGIN'); return $response; } catch (Exception $e) { return new WP_Error( 'preview_generation_failed', __('Failed to generate email preview: ', 'activity-log-pro') . $e->getMessage(), array('status' => 500) ); } } /** * REST API endpoint callback for sending test email. * * Sends test email digest to specified email address for * delivery verification and content review. * * @param WP_REST_Request $request REST request object. * @return WP_REST_Response|WP_Error Success response or error message. * @since 1.1.0 */ public static function rest_send_test($request) { // Get and validate email address $email = $request->get_param('email'); if (empty($email)) { $email = get_option('admin_email'); } // Sanitize email $email = sanitize_email($email); // Validate email format if (!is_email($email)) { return new WP_Error( 'invalid_email', __('Please provide a valid email address.', 'activity-log-pro'), array('status' => 400) ); } // Get sample data for test email $sample_data = self::get_digest_data(true); // Generate email content $html_content = self::generate_email_html($sample_data); $subject = self::generate_email_subject(true); // Send test email — suppress mail logging for ALP's own emails. if ( class_exists( 'ACTLOGPRO_Mail_Logger' ) ) { ACTLOGPRO_Mail_Logger::suppress(); } $sent = wp_mail( $email, $subject, $html_content, array('Content-Type: text/html; charset=UTF-8') ); if ( class_exists( 'ACTLOGPRO_Mail_Logger' ) ) { ACTLOGPRO_Mail_Logger::resume(); } if ($sent) { return rest_ensure_response(array( 'success' => true, 'message' => sprintf( /* translators: %s: Email address */ __('Test digest email sent successfully to %s.', 'activity-log-pro'), $email ) )); } else { return new WP_Error( 'email_send_failed', __('Failed to send test digest email.', 'activity-log-pro'), array('status' => 500) ); } } /** * Schedule automated email digest delivery if not already scheduled. * * Follows WordPress best practices by checking if already scheduled * before creating new cron events. Only schedules when enabled. * * @param bool $force_reschedule Skip duplicate check and always reschedule * @return void * @since 1.1.0 */ public static function schedule_email_digest($force_reschedule = false) { // Only schedule if feature is enabled if (!self::is_email_digest_enabled()) { // Clear any existing schedule if feature is disabled wp_clear_scheduled_hook(self::CRON_HOOK); return; } // Check if already properly scheduled to avoid duplicates $next_scheduled = wp_next_scheduled(self::CRON_HOOK); $frequency = get_option('actlogpro_email_digest_frequency', 'weekly'); $day = get_option('actlogpro_email_digest_day', 'monday'); $time = get_option('actlogpro_email_digest_time', '08:00'); // If already scheduled, check if the frequency matches (unless forcing reschedule) if ($next_scheduled && !$force_reschedule) { $current_schedule = wp_get_schedule(self::CRON_HOOK); if ($current_schedule === $frequency) { return; // Already correctly scheduled } // Frequency changed, reschedule wp_clear_scheduled_hook(self::CRON_HOOK); } // If forcing reschedule and already scheduled, clear it first if ($force_reschedule && $next_scheduled) { wp_clear_scheduled_hook(self::CRON_HOOK); } // Calculate next run timestamp based on frequency $next_run = self::calculate_next_run_time($frequency, $day, $time); if ($next_run) { wp_schedule_event($next_run, $frequency, self::CRON_HOOK); } } /** * Force reschedule of email digest (used when settings change). * * Clears existing schedule and creates new one based on current settings. * This method should only be called when settings are updated. * * @return void * @since 1.1.0 */ public static function reschedule_email_digest() { // Clear existing schedule wp_clear_scheduled_hook(self::CRON_HOOK); // Clear options cache to ensure we get fresh settings values wp_cache_delete('actlogpro_email_digest_frequency', 'options'); wp_cache_delete('actlogpro_email_digest_day', 'options'); wp_cache_delete('actlogpro_email_digest_time', 'options'); wp_cache_delete('actlogpro_email_digest_enabled', 'options'); // Schedule new one if enabled (force reschedule to bypass duplicate check) if (self::is_email_digest_enabled()) { self::schedule_email_digest(true); // Force reschedule } } /** * Calculate next scheduled run time based on frequency settings. * * Determines the appropriate timestamp for next email delivery * based on frequency (daily/weekly/monthly), day, and time settings. * * @param string $frequency Email frequency (daily/weekly/monthly). * @param string $day Day setting (weekday name or day number). * @param string $time Time in HH:MM format. * @return int|false Timestamp for next run, or false on error. * @since 1.1.0 */ private static function calculate_next_run_time($frequency, $day, $time) { $timezone = new DateTimeZone(wp_timezone_string()); try { switch ($frequency) { case 'daily': $next_run = new DateTime("today {$time}", $timezone); if ($next_run->getTimestamp() <= time()) { $next_run = new DateTime("tomorrow {$time}", $timezone); } return $next_run->getTimestamp(); case 'weekly': $next_run = new DateTime("next {$day} {$time}", $timezone); return $next_run->getTimestamp(); case 'monthly': $next_run = new DateTime("first day of next month {$time}", $timezone); $next_run->setDate( $next_run->format('Y'), $next_run->format('n'), min((int) $day, $next_run->format('t')) ); return $next_run->getTimestamp(); } } catch (Exception $e) { // Log error but don't break functionality if (defined('WP_DEBUG') && WP_DEBUG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Intentional debug logging when WP_DEBUG is enabled error_log('Activity Log Pro: Error calculating next email digest run time - ' . $e->getMessage()); } } return false; } /** * Send automated email digest to configured recipients. * * Main cron callback function that generates and sends * email digest reports to all configured recipients. * * @return void * @since 1.1.0 */ public static function send_email_digest() { $execution_context = defined('WP_CLI') && WP_CLI ? 'WP-CLI' : 'WordPress Cron'; // Verify feature is still enabled if (!self::is_email_digest_enabled()) { return; } // Get recipients list $recipients = self::get_recipients(); if (empty($recipients)) { return; } try { // Get digest data $digest_data = self::get_digest_data(false); // Generate email content $html_content = self::generate_email_html($digest_data); $subject = self::generate_email_subject(false); if (empty($html_content)) { if (defined('WP_DEBUG') && WP_DEBUG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Intentional debug logging when WP_DEBUG is enabled error_log('Activity Log Pro: Failed to generate email digest content'); } return; } // Send to each recipient with delivery tracking — suppress mail logging for ALP's own emails. $sent_count = 0; $failed_count = 0; foreach ($recipients as $recipient) { if ( class_exists( 'ACTLOGPRO_Mail_Logger' ) ) { ACTLOGPRO_Mail_Logger::suppress(); } $sent = wp_mail( trim($recipient), $subject, $html_content, array('Content-Type: text/html; charset=UTF-8') ); if ( class_exists( 'ACTLOGPRO_Mail_Logger' ) ) { ACTLOGPRO_Mail_Logger::resume(); } if ($sent) { $sent_count++; } else { $failed_count++; // Log failures for troubleshooting if (defined('WP_DEBUG') && WP_DEBUG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Intentional debug logging when WP_DEBUG is enabled error_log('Activity Log Pro: Failed to send email digest to ' . trim($recipient)); } } } // Update last sent timestamp update_option('actlogpro_email_digest_last_sent', time()); // Log successful delivery for admin reference if ($sent_count > 0) { ACTLOGPRO_Logger::log_activity( 'system', 'email_digest', 0, 'Email Digest Report', 'sent', sprintf('Email digest sent successfully to %d of %d recipients via %s', $sent_count, count($recipients), $execution_context), array( 'recipients_count' => $sent_count, 'total_recipients' => count($recipients), 'failed_count' => $failed_count, 'execution_context' => $execution_context ) ); } } catch (Exception $e) { if (defined('WP_DEBUG') && WP_DEBUG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Intentional debug logging when WP_DEBUG is enabled error_log('Activity Log Pro: Email digest failed - ' . $e->getMessage()); } } } /** * Generate comprehensive digest data for email reports. * * Integrates with existing stats system to gather activity data * for specified time period. Includes security events, business * activities, and database statistics. * * @param bool $is_preview Whether this is for preview/test purposes. * @return array Comprehensive digest data array. * @since 1.1.0 */ public static function get_digest_data($is_preview = false) { $frequency = get_option('actlogpro_email_digest_frequency', 'weekly'); // Calculate time range based on frequency // Use WordPress timezone-aware functions to match database timestamps if ($frequency === 'daily') { // For preview/test mode, always show today's data to make testing easier if ($is_preview) { $date_from = strtotime(current_time('Y-m-d') . ' 00:00:00'); $date_to = current_time('timestamp'); // Current time in site timezone } else { // For actual scheduled emails: determine if we should show yesterday's or today's data // If email is scheduled before noon (12:00 PM), show yesterday's complete data // If email is scheduled at/after noon, show today's data up to current time $scheduled_time = get_option('actlogpro_email_digest_time', '08:00'); list($hour, $minute) = explode(':', $scheduled_time); $scheduled_hour = intval($hour); if ($scheduled_hour < 12) { // Morning delivery: show yesterday's complete activity $yesterday = gmdate('Y-m-d', strtotime('-1 day', current_time('timestamp'))); $date_from = strtotime($yesterday . ' 00:00:00'); $date_to = strtotime($yesterday . ' 23:59:59'); } else { // Afternoon/evening delivery: show today's activity so far $date_from = strtotime(current_time('Y-m-d') . ' 00:00:00'); $date_to = current_time('timestamp'); // Current time in site timezone } } } else { // For weekly/monthly: use rolling period as before $days_back = ($frequency === 'weekly') ? 7 : 30; $date_to = current_time('timestamp'); $date_from = strtotime("-{$days_back} days", $date_to); } // Basic site information $digest_data = array( 'site_name' => get_bloginfo('name'), 'site_url' => get_bloginfo('url'), 'site_url_domain' => wp_parse_url(get_bloginfo('url'), PHP_URL_HOST), 'frequency' => $frequency, 'date_range' => self::format_date_range($date_from, $date_to), 'is_preview' => $is_preview ); // Get total activity statistics using existing stats system $digest_data['total_events'] = ACTLOGPRO_Database::get_total_logs_count(); $digest_data['period_events'] = ACTLOGPRO_Database::get_logs_count_by_date_range( gmdate('Y-m-d H:i:s', $date_from), gmdate('Y-m-d H:i:s', $date_to) ); // Get database size information using existing statistics method $db_stats = ACTLOGPRO_Database::get_log_statistics(); $digest_data['total_log_entries'] = $db_stats['count']; $digest_data['database_size'] = $db_stats['size']; // Integrate security statistics using existing helpers $security_stats = self::get_security_digest_stats($date_from, $date_to); $digest_data = array_merge($digest_data, $security_stats); // Integrate business activity statistics $business_stats = self::get_business_digest_stats($date_from, $date_to); $digest_data = array_merge($digest_data, $business_stats); // Check for WooCommerce integration data if premium and available if (self::is_woocommerce_data_available()) { $wc_stats = self::get_woocommerce_digest_stats($date_from, $date_to); $digest_data['woocommerce'] = $wc_stats; } return $digest_data; } /** * Generate security-focused statistics for email digest. * * Leverages existing security stats system to provide comprehensive * security activity summary with emphasis on critical events. * * @param int $date_from Start timestamp for analysis. * @param int $date_to End timestamp for analysis. * @return array Security statistics array. * @since 1.1.0 */ private static function get_security_digest_stats($date_from, $date_to) { // Use existing stats helper to get security data $security_data = ACTLOGPRO_Database::get_security_stats_for_period($date_from, $date_to); return array( 'failed_logins' => $security_data['failed_logins'], 'successful_logins' => $security_data['successful_logins'], 'new_users' => $security_data['new_users'], 'password_changes' => $security_data['password_changes'], 'suspicious_activities' => $security_data['suspicious_activities'], 'critical_changes' => $security_data['critical_changes'] ); } /** * Generate business activity statistics for email digest. * * Integrates with existing business stats to provide content * and administrative activity summaries. * * @param int $date_from Start timestamp for analysis. * @param int $date_to End timestamp for analysis. * @return array Business activity statistics array. * @since 1.1.0 */ private static function get_business_digest_stats($date_from, $date_to) { // Use existing stats helper to get business activity data $business_data = ACTLOGPRO_Database::get_business_stats_for_period($date_from, $date_to); return array( 'posts_created' => $business_data['posts_created'], 'posts_updated' => $business_data['posts_updated'], 'media_uploads' => $business_data['media_uploads'], 'plugin_activities' => $business_data['plugin_activities'], 'theme_activities' => $business_data['theme_activities'], 'admin_changes' => $business_data['admin_changes'], 'wordpress_updates' => $business_data['wordpress_updates'] ); } /** * Check if WooCommerce integration data is available. * * Verifies if premium version is active and WooCommerce * integration is providing trackable data. * * @return bool True if WooCommerce data is available. * @since 1.1.0 */ private static function is_woocommerce_data_available() { // Check if premium is active and WooCommerce is available return ( ACTLOGPRO_Premium_Helper::is_premium_enabled() && class_exists('WooCommerce') && ACTLOGPRO_Database::has_woocommerce_activity() ); } /** * Generate WooCommerce-specific digest statistics. * * Provides e-commerce activity summary when premium * WooCommerce integration is available. * * @param int $date_from Start timestamp for analysis. * @param int $date_to End timestamp for analysis. * @return array WooCommerce statistics array. * @since 1.1.0 */ private static function get_woocommerce_digest_stats($date_from, $date_to) { return ACTLOGPRO_Database::get_woocommerce_stats_for_period($date_from, $date_to); } /** * Format date range for human-readable display. * * Creates formatted date range string using WordPress * localization functions for international compatibility. * * @param int $date_from Start timestamp. * @param int $date_to End timestamp. * @return string Formatted date range string. * @since 1.1.0 */ private static function format_date_range($date_from, $date_to) { $date_format = get_option('date_format'); return sprintf( /* translators: 1: start date, 2: end date */ __('%1$s – %2$s', 'activity-log-pro'), date_i18n($date_format, $date_from), date_i18n($date_format, $date_to) ); } /** * Generate professional email subject line. * * Creates informative subject line with site name * and optional preview indicator. * * @param bool $is_preview Whether this is a preview/test email. * @return string Email subject line. * @since 1.1.0 */ private static function generate_email_subject($is_preview = false) { $subject = sprintf( /* translators: %s: Site name */ __('Activity Digest Report for %s', 'activity-log-pro'), get_bloginfo('name') ); if ($is_preview) { $subject .= ' ' . __('(Preview)', 'activity-log-pro'); } return $subject; } /** * Generate HTML email template content. * * Creates professional, responsive HTML email template * with Activity Log Pro branding and comprehensive content. * * @param array $data Digest data array for template population. * @return string Complete HTML email content. * @since 1.1.0 */ private static function generate_email_html($data) { $template_path = ACTLOGPRO_PLUGIN_DIR . 'admin/partials/email-digest-template.php'; // Assign data to prefixed variable for template (WordPress Coding Standards compliance) $actlogpro_data = $data; // Load email template ob_start(); if (!file_exists($template_path)) { ob_end_clean(); return '

Template Error

Email template file not found: ' . esc_html($template_path) . '

'; } try { include $template_path; $content = ob_get_clean(); return $content; } catch (Exception $e) { ob_end_clean(); return '

Template Error

Error loading template: ' . esc_html($e->getMessage()) . '

'; } } /** * Sanitize checkbox values to match WordPress standards. * * Ensures checkbox values are properly handled as '1' or '0' strings. * * @param mixed $value Input value to sanitize. * @return string '1' if truthy, '0' if falsy. * @since 1.1.0 */ public static function sanitize_checkbox($value) { return !empty($value) ? '1' : '0'; } /** * Check if email digest feature is currently enabled. * * @return bool True if enabled, false otherwise. * @since 1.1.0 */ public static function is_email_digest_enabled() { $value = get_option('actlogpro_email_digest_enabled', '0'); return $value === '1'; } /** * Get array of configured email recipients. * * @return array Array of email addresses. * @since 1.1.0 */ private static function get_recipients() { $recipients = get_option('actlogpro_email_digest_recipients', ''); return empty($recipients) ? array() : explode("\n", $recipients); } /** * Enqueue admin scripts for digest functionality. * * @param string $hook_suffix Current admin page hook suffix. * @since 1.1.0 */ public static function enqueue_admin_scripts($hook_suffix) { // Only load on Activity Log Pro settings pages if (strpos($hook_suffix, 'activity-log-pro') === false) { return; } wp_enqueue_script( 'actlogpro-email-digest', ACTLOGPRO_PLUGIN_URL . 'admin/js/alp-email-digest.js', array('jquery', 'wp-api-fetch'), ACTLOGPRO_VERSION, true ); wp_localize_script( 'actlogpro-email-digest', 'actlogproEmailDigest', array( 'nonce' => wp_create_nonce('wp_rest'), 'previewNonce' => wp_create_nonce('actlogpro_email_preview'), 'previewUrl' => add_query_arg(array( 'actlogpro_email_preview' => '1' ), admin_url('admin.php')), 'testUrl' => rest_url(self::REST_NAMESPACE . '/email-digest/preview/send-test'), 'strings' => array( 'sendingTest' => __('Sending test email...', 'activity-log-pro'), 'testSent' => __('Test email sent successfully!', 'activity-log-pro'), 'testFailed' => __('Failed to send test email.', 'activity-log-pro'), 'invalidEmail' => __('Please enter a valid email address.', 'activity-log-pro'), 'loadingPreview' => __('Loading preview...', 'activity-log-pro'), 'previewFailed' => __('Failed to load preview.', 'activity-log-pro') ) ) ); // Add CSS for preview popup wp_enqueue_style( 'actlogpro-email-digest', ACTLOGPRO_PLUGIN_URL . 'admin/css/alp-email-digest.css', array(), ACTLOGPRO_VERSION ); } } /** * Global sanitization function for email digest checkbox. * * @param mixed $value Input value to sanitize. * @return string '1' if truthy, '0' if falsy. */ function actlogpro_email_digest_sanitize_checkbox($value) { return !empty($value) ? '1' : '0'; }