Linux webm002.cluster126.gra.hosting.ovh.net 6.18.39-ovh-vps-grsec-zfs+ #1 SMP PREEMPT_DYNAMIC Tue Jul 21 12:03:15 CEST 2026 x86_64
/
home
/
ariannadhf
/
www
/
wp-content
/
plugins
/
simple-history
/
inc
/
services
/
/home/ariannadhf/www/wp-content/plugins/simple-history/inc/services/class-email-report-service.php
<?php namespace Simple_History\Services; use Simple_History\Helpers; use Simple_History\Events_Stats; use Simple_History\Date_Helper; use Simple_History\Menu_Page; /** * Service that handles email reports. */ class Email_Report_Service extends Service { private const SETTINGS_PAGE_SLUG = 'simple_history_settings_menu_slug_email_reports'; private const SETTINGS_OPTION_GROUP = 'simple_history_settings_group_email_reports'; /** * @inheritdoc */ public function loaded() { // Register settings and menu tab. add_action( 'admin_menu', [ $this, 'add_settings_menu_tab' ], 15 ); add_action( 'admin_menu', [ $this, 'register_settings' ], 13 ); // Register REST API endpoints. add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] ); // Schedule email report. add_action( 'init', [ $this, 'schedule_email_report' ] ); add_action( 'simple_history/email_report', [ $this, 'send_email_report' ] ); // Handle enable/disable of email reports. add_action( 'update_option_simple_history_email_report_enabled', [ $this, 'on_email_report_enabled_updated' ], 10, 2 ); } /** * Register REST API routes. */ public function register_rest_routes() { register_rest_route( 'simple-history/v1', '/email-report/preview/email', [ 'methods' => [ \WP_REST_Server::CREATABLE, \WP_REST_Server::READABLE ], 'callback' => [ $this, 'rest_preview_email' ], 'permission_callback' => [ $this, 'rest_permission_callback' ], ] ); register_rest_route( 'simple-history/v1', '/email-report/preview/html', [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'rest_preview_html' ], 'permission_callback' => [ $this, 'rest_permission_callback' ], ] ); } /** * Permission callback for REST API endpoints. */ public function rest_permission_callback() { return current_user_can( 'manage_options' ); } /** * Add email reports settings tab as a subtab to main settings tab. */ public function add_settings_menu_tab() { $menu_manager = $this->simple_history->get_menu_manager(); // Bail if parent settings page does not exist (due to Stealth Mode or similar). if ( ! $menu_manager->page_exists( Setup_Settings_Page::SETTINGS_GENERAL_SUBTAB_SLUG ) ) { return; } ( new Menu_Page() ) ->set_page_title( __( 'Email Reports', 'simple-history' ) ) ->set_menu_title( __( 'Email Reports', 'simple-history' ) ) ->set_menu_slug( 'general_settings_subtab_email_reports' ) ->set_callback( [ $this, 'settings_output_email_reports' ] ) ->set_order( 15 ) ->set_parent( Setup_Settings_Page::SETTINGS_GENERAL_SUBTAB_SLUG ) ->add(); } /** * Output for the email reports settings tab. */ public function settings_output_email_reports() { ?> <div class="wrap sh-Page-content"> <form method="post" action="options.php"> <?php do_settings_sections( self::SETTINGS_PAGE_SLUG ); settings_fields( self::SETTINGS_OPTION_GROUP ); submit_button(); ?> </form> </div> <?php } /** * Prepare top items array with safe fallbacks. * * @param array $items Raw items array. * @param int $limit Maximum number of items to return. * @param string $name_key Key for the name field. * @param string $count_key Key for the count field. * @return array */ private function prepare_top_items( $items, $limit = 3, $name_key = 'name', $count_key = 'count' ) { $result = []; if ( ! is_array( $items ) || empty( $items ) ) { return array_fill( 0, $limit, [ 'name' => '', 'count' => 0, ] ); } for ( $i = 0; $i < $limit; $i++ ) { if ( isset( $items[ $i ] ) ) { $item = $items[ $i ]; $result[] = [ 'name' => is_object( $item ) ? $item->$name_key : $item[ $name_key ], 'count' => is_object( $item ) ? $item->$count_key : $item[ $count_key ], ]; } else { $result[] = [ 'name' => '', 'count' => 0, ]; } } return $result; } /** * Get summary report data for a given date range. * * Note: Event counts include ALL logged events, including those from experimental/verbose * loggers like WPCronLogger, WPRESTAPIRequestsLogger, and WPHTTPRequestsLogger. These loggers * can generate hundreds of events per day from background system activity. * * This can cause a mismatch between email stats and what users see in the log UI, because: * 1. Experimental loggers may be disabled after events were logged * 2. The log UI filters events based on user permissions and enabled loggers * 3. Users clicking through from email may see fewer events than reported * * Potential future solutions: * - Filter stats to only include "standard" loggers (exclude experimental/verbose ones) * - Show breakdown in email: "X user events + Y system events" * - Add disclaimer text explaining the mismatch * - Make day links include all events regardless of current logger settings * * @param int $date_from Start timestamp. * @param int $date_to End timestamp. * @param bool $is_preview Whether this is a preview email. * @return array */ public function get_summary_report_data( $date_from, $date_to, $is_preview = false ) { // Get stats for the specified period. $events_stats = new Events_Stats(); // Get basic site info. $stats = [ 'site_name' => get_bloginfo( 'name' ), 'site_url' => get_bloginfo( 'url' ), 'site_url_domain' => wp_parse_url( get_bloginfo( 'url' ), PHP_URL_HOST ), // Date range as string, as it's displayed in the email. 'date_range' => sprintf( /* translators: 1: start date with day name, 2: end date with day name, 3: year */ __( '%1$s – %2$s, %3$s', 'simple-history' ), wp_date( sprintf( /* translators: %s is the site's date format setting without year */ __( 'D %s', 'simple-history' ), // Remove year from date format. trim( preg_replace( '/[,\s]*[YyoL][,\s]*/', '', get_option( 'date_format' ) ), ', ' ) ), $date_from ), wp_date( sprintf( /* translators: %s is the site's date format setting without year */ __( 'D %s', 'simple-history' ), // Remove year from date format. trim( preg_replace( '/[,\s]*[YyoL][,\s]*/', '', get_option( 'date_format' ) ), ', ' ) ), $date_to ), wp_date( 'Y', $date_to ) ), 'email_subject' => $this->get_email_subject( $is_preview ), ]; // Get total events for this week. $stats['total_events_this_week'] = $events_stats->get_total_events( $date_from, $date_to ); // Get all days with event counts for the template. // Don't limit or sort - template will use all days in chronological order. $peak_days = $events_stats->get_peak_days( $date_from, $date_to ); // Convert to array format for template (handle both empty and populated results). // Use day numbers (0-6) instead of translated names to avoid language issues. $all_days = []; if ( $peak_days && is_array( $peak_days ) ) { foreach ( $peak_days as $day ) { $all_days[] = [ // phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- This explains value range. 'day_number' => $day->day, // 0=Sunday and 6=Saturday. 'count' => $day->count, ]; } } $stats['most_active_days'] = $all_days; // Find the busiest day (day with the highest count). $busiest_day_name = __( 'No activity', 'simple-history' ); if ( ! empty( $all_days ) ) { $max_count = 0; $busiest_day_number = 0; foreach ( $all_days as $day ) { if ( $day['count'] <= $max_count ) { continue; } $max_count = $day['count']; $busiest_day_number = $day['day_number']; } // Only set the busiest day if there was actual activity. if ( $max_count > 0 ) { // Convert day number to localized day name. // day_number: 0=Sunday, 1=Monday, etc. $day_names = [ 0 => __( 'Sunday', 'simple-history' ), 1 => __( 'Monday', 'simple-history' ), 2 => __( 'Tuesday', 'simple-history' ), 3 => __( 'Wednesday', 'simple-history' ), 4 => __( 'Thursday', 'simple-history' ), 5 => __( 'Friday', 'simple-history' ), 6 => __( 'Saturday', 'simple-history' ), ]; $busiest_day_name = $day_names[ $busiest_day_number ] ?? __( 'No activity', 'simple-history' ); } } $stats['busiest_day_name'] = $busiest_day_name; // Pass date range timestamps for chronological day ordering in template. $stats['date_from_timestamp'] = $date_from; $stats['date_to_timestamp'] = $date_to; // Get most active users and format them for the template. $top_users = $events_stats->get_top_users( $date_from, $date_to, 3 ); $stats['most_active_users'] = $this->prepare_top_items( $top_users, 3, 'display_name', 'count' ); // Get user login statistics. $stats['successful_logins'] = $events_stats->get_successful_logins_count( $date_from, $date_to ); $stats['failed_logins'] = $events_stats->get_failed_logins_count( $date_from, $date_to ); // Get posts statistics. $stats['posts_created'] = $events_stats->get_posts_pages_created( $date_from, $date_to ); $stats['posts_updated'] = $events_stats->get_posts_pages_updated( $date_from, $date_to ); // Get plugins statistics. $stats['plugin_activations'] = $events_stats->get_plugin_activations_count( $date_from, $date_to ); $stats['plugin_deactivations'] = $events_stats->get_plugin_deactivations_count( $date_from, $date_to ); // Get user creation and modification statistics. $stats['users_created'] = $events_stats->get_user_added_count( $date_from, $date_to ); $stats['users_updated'] = $events_stats->get_user_updated_count( $date_from, $date_to ); // Get media statistics. $stats['media_uploads'] = $events_stats->get_media_uploads_count( $date_from, $date_to ); $stats['media_edits'] = $events_stats->get_media_edits_count( $date_from, $date_to ); // Get comments statistics (only query when comments are enabled). $stats['comments_enabled'] = get_option( 'default_comment_status' ) === 'open'; if ( $stats['comments_enabled'] ) { $stats['comments_added'] = $events_stats->get_comments_added_count( $date_from, $date_to ); $stats['comments_approved'] = $events_stats->get_comments_approved_count( $date_from, $date_to ); $stats['comments_spam'] = $events_stats->get_comments_spam_count( $date_from, $date_to ); } // Get theme statistics. $stats['theme_switches'] = $events_stats->get_theme_switches_count( $date_from, $date_to ); $stats['theme_updates'] = $events_stats->get_theme_updates_count( $date_from, $date_to ); // Get WordPress core statistics. $stats['wordpress_updates'] = $events_stats->get_wordpress_core_updates_count( $date_from, $date_to ); // Get Notes statistics (WordPress 6.9+). global $wp_version; $stats['notes_enabled'] = version_compare( $wp_version, '6.9', '>=' ); $stats['notes_added'] = $events_stats->get_notes_added_count( $date_from, $date_to ); $stats['notes_resolved'] = $events_stats->get_notes_resolved_count( $date_from, $date_to ); // Add history admin URL. $stats['history_admin_url'] = \Simple_History\Helpers::get_history_admin_url(); // Add settings URL for unsubscribe link. $stats['settings_url'] = admin_url( 'admin.php?page=simple_history_settings_page&selected-tab=general_settings_subtab_general&selected-sub-tab=general_settings_subtab_email_reports' ); return $stats; } /** * Generate email subject for reports. * * @param bool $is_preview Whether this is a preview email. * @return string */ private function get_email_subject( $is_preview = false ) { $subject = sprintf( // translators: %s: Site name. __( 'Weekly Activity Summary for %s', 'simple-history' ), get_bloginfo( 'name' ) ); if ( $is_preview ) { $subject .= ' (preview)'; } return $subject; } /** * REST API endpoint for sending preview email. */ public function rest_preview_email() { $current_user = wp_get_current_user(); // Preview shows last 7 days including today, matching sidebar "7 days" stat. $date_range = Date_Helper::get_last_n_days_range( Date_Helper::DAYS_PER_WEEK ); $date_from = $date_range['from']; $date_to = $date_range['to']; ob_start(); load_template( SIMPLE_HISTORY_PATH . 'templates/email-summary-report.php', false, $this->get_summary_report_data( $date_from, $date_to, true ) ); $email_content = ob_get_clean(); $subject = $this->get_email_subject( true ); $headers = [ 'Content-Type: text/html; charset=UTF-8' ]; // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail -- Not bulk, this is a preview email sent to a single recipient. $sent = wp_mail( $current_user->user_email, $subject, $email_content, $headers ); if ( $sent ) { return rest_ensure_response( [ 'success' => true, 'message' => sprintf( /* translators: %s: Email address */ __( 'Test email sent successfully to %s.', 'simple-history' ), $current_user->user_email ), ] ); } return new \WP_Error( 'email_send_failed', __( 'Failed to send test email.', 'simple-history' ), [ 'status' => 500 ] ); } /** * REST API endpoint for getting HTML preview. */ public function rest_preview_html() { // Preview shows last 7 days including today, matching sidebar "7 days" stat. $date_range = Date_Helper::get_last_n_days_range( Date_Helper::DAYS_PER_WEEK ); $date_from = $date_range['from']; $date_to = $date_range['to']; // Set content type to HTML. header( 'Content-Type: text/html; charset=UTF-8' ); load_template( SIMPLE_HISTORY_PATH . 'templates/email-summary-report.php', false, $this->get_summary_report_data( $date_from, $date_to, true ) ); exit; } /** * Register settings for email report. */ public function register_settings() { // Add settings section for email reports. Helpers::add_settings_section( 'simple_history_email_report_section', [ __( 'Email Reports (Weekly Activity Digest)', 'simple-history' ), 'schedule_send', 'simple_history_email_report_section' ], [ $this, 'settings_section_output' ], self::SETTINGS_PAGE_SLUG, [ 'callback_last' => [ $this, 'settings_section_output_last' ], ], ); register_setting( self::SETTINGS_OPTION_GROUP, 'simple_history_email_report_enabled', [ 'type' => 'boolean', 'default' => false, 'sanitize_callback' => 'rest_sanitize_boolean', ] ); register_setting( self::SETTINGS_OPTION_GROUP, 'simple_history_email_report_recipients', [ 'type' => 'string', 'default' => '', 'sanitize_callback' => [ $this, 'sanitize_email_recipients' ], ] ); // Add settings fields directly (no longer via hook). add_settings_field( 'simple_history_email_report_enabled', Helpers::get_settings_field_title_output( __( 'Enable', 'simple-history' ), 'mark_email_unread' ), [ $this, 'settings_field_enabled' ], self::SETTINGS_PAGE_SLUG, 'simple_history_email_report_section' ); add_settings_field( 'simple_history_email_report_recipients', Helpers::get_settings_field_title_output( __( 'Recipients', 'simple-history' ), 'group_add' ), [ $this, 'settings_field_recipients' ], self::SETTINGS_PAGE_SLUG, 'simple_history_email_report_section' ); add_settings_field( 'simple_history_email_report_preview', Helpers::get_settings_field_title_output( __( 'Preview', 'simple-history' ), 'preview' ), [ $this, 'settings_field_preview' ], self::SETTINGS_PAGE_SLUG, 'simple_history_email_report_section' ); } /** * Output for the last content of the email report settings section. */ public function settings_section_output_last() { echo '<p>💡 ' . esc_html__( 'Pro tip: The digest helps you catch unauthorized changes even when you\'re away from your site.', 'simple-history' ) . '</p>'; } /** * Output for the email report settings section. */ public function settings_section_output() { ?> <p> <strong><?php esc_html_e( 'Stay on top of your site without logging in.', 'simple-history' ); ?></strong> </p> <?php } /** * Output for the preview and test setting field. */ public function settings_field_preview() { $current_user = wp_get_current_user(); $preview_url = add_query_arg( [ '_wpnonce' => wp_create_nonce( 'wp_rest' ), ], rest_url( 'simple-history/v1/email-report/preview/html' ) ); ?> <div> <p> <a href="<?php echo esc_url( $preview_url ); ?>" target="_blank" class="button button-link"> <?php esc_html_e( 'Show email preview', 'simple-history' ); ?> </a> | <button type="button" class="button button-link" id="simple-history-email-test"> <?php printf( // translators: %s: Current user's email address. esc_html__( 'Send test email to %s', 'simple-history' ), esc_html( $current_user->user_email ) ); ?> </button> </p> </div> <script> jQuery(document).ready(function($) { // Handle test email $('#simple-history-email-test').on('click', function() { wp.apiFetch({ path: '/simple-history/v1/email-report/preview/email', method: 'POST' }).then(function(response) { alert(response.message); }).catch(function(error) { alert('<?php esc_html_e( 'Failed to send test email.', 'simple-history' ); ?>'); }); }); }); </script> <?php } /** * Output for the pro tip field. */ public function settings_field_pro_tip() { ?> <div class="sh-EmailReportProTip"> <p> 💡 <?php esc_html_e( 'Pro tip: The digest helps you catch unauthorized changes even when you\'re away from your site.', 'simple-history' ); ?> </p> </div> <?php } /** * Sanitize email recipients. * * Detects all emails in textarea and sanitizes them. * Emails are separated by spaces, commas or newlines. * Final result is a string with one email address per line (separated by \n). * * @param string $textarea_contents Textarea contents. * @return string */ public function sanitize_email_recipients( $textarea_contents ) { // First remove tags and scripts etc. $textarea_contents = sanitize_textarea_field( $textarea_contents ); // Convert to array and sanitize each email. // Spaces or newlines are valid splits. $textarea_contents = preg_split( '/[\s,]+/', $textarea_contents ); // Validate each item using WordPress is_email() function. $textarea_contents = array_filter( $textarea_contents, 'is_email' ); // Remove duplicates and reindex array. $textarea_contents = array_values( array_unique( $textarea_contents ) ); // Join back to string. $textarea_contents = implode( "\n", $textarea_contents ); return $textarea_contents; } /** * Check if email reports are enabled. * * @return bool */ private function is_email_reports_enabled() { return get_option( 'simple_history_email_report_enabled', false ); } /** * Get email report recipients. * * @return string One email address per line (separated by \n). */ private function get_email_report_recipients() { return get_option( 'simple_history_email_report_recipients', '' ); } /** * Output for the enabled setting field. */ public function settings_field_enabled() { $enabled = $this->is_email_reports_enabled(); ?> <label> <input type="checkbox" name="simple_history_email_report_enabled" value="1" <?php checked( $enabled ); ?> /> <?php esc_html_e( 'Enable weekly digest', 'simple-history' ); ?> </label> <p style="margin-top: 1em;"> <?php esc_html_e( 'Every Monday, get a summary of:', 'simple-history' ); ?> </p> <ul style="list-style: none; padding: 0; margin: 0.5em 0 0 0;"> <li><span class="dashicons dashicons-yes" style="color: #00a32a; margin-inline-end: 0.25em;"></span><?php esc_html_e( 'Total event count and daily breakdown', 'simple-history' ); ?></li> <li><span class="dashicons dashicons-yes" style="color: #00a32a; margin-inline-end: 0.25em;"></span><?php esc_html_e( 'Number of posts and pages created or updated', 'simple-history' ); ?></li> <li><span class="dashicons dashicons-yes" style="color: #00a32a; margin-inline-end: 0.25em;"></span><?php esc_html_e( 'Login statistics (successful and failed)', 'simple-history' ); ?></li> <li><span class="dashicons dashicons-yes" style="color: #00a32a; margin-inline-end: 0.25em;"></span><?php esc_html_e( 'User creation and profile update counts', 'simple-history' ); ?></li> <li><span class="dashicons dashicons-yes" style="color: #00a32a; margin-inline-end: 0.25em;"></span><?php esc_html_e( 'Plugin activation and deactivation counts', 'simple-history' ); ?></li> <li><span class="dashicons dashicons-yes" style="color: #00a32a; margin-inline-end: 0.25em;"></span><?php esc_html_e( 'WordPress core update count', 'simple-history' ); ?></li> </ul> <?php } /** * Output for the email recipients field. */ public function settings_field_recipients() { $recipients = $this->get_email_report_recipients(); $current_user_email = wp_get_current_user()->user_email; ?> <p> <?php esc_html_e( 'Add team members to keep everyone informed.', 'simple-history' ); ?> </p> <textarea data-simple-history-email-report-recipients data-simple-history-current-user-email="<?php echo esc_attr( $current_user_email ); ?>" placeholder="email@example.com another@example.com" style="field-sizing: content; min-width: 20rem; min-height: 3rem;" name="simple_history_email_report_recipients" id="simple_history_email_report_recipients" class="regular-text" rows="5" cols="50" ><?php echo esc_textarea( $recipients ); ?></textarea> <p class="description"> <?php esc_html_e( 'Enter one email address per line.', 'simple-history' ); ?> </p> <?php } /** * Schedule the email report. */ public function schedule_email_report() { // Bail if email reports are not enabled. if ( ! $this->is_email_reports_enabled() ) { return; } // Bail if email report is already scheduled. if ( wp_next_scheduled( 'simple_history/email_report' ) ) { return; } // Schedule for next Monday at 6:00 AM in WordPress timezone. $next_monday = new \DateTimeImmutable( 'next monday 6:00:00', wp_timezone() ); wp_schedule_event( $next_monday->getTimestamp(), 'weekly', 'simple_history/email_report' ); } /** * Unschedule the email report. */ public function unschedule_email_report() { wp_clear_scheduled_hook( 'simple_history/email_report' ); } /** * Handle when email report enabled setting is updated. * * @param mixed $_old_value Old value (unused). * @param mixed $new_value New value. */ public function on_email_report_enabled_updated( $_old_value, $new_value ) { if ( $new_value ) { $this->schedule_email_report(); } else { $this->unschedule_email_report(); } } /** * Send the email report. */ public function send_email_report() { // Check if email report is enabled. if ( ! $this->is_email_reports_enabled() ) { return; } $recipients = $this->get_email_report_recipients(); if ( empty( $recipients ) ) { return; } // Convert from newline string to array. $recipients = explode( "\n", $recipients ); // Get stats for last complete week (Monday-Sunday). // Sent on Mondays, shows previous Mon-Sun, excludes current Monday. $date_range = Date_Helper::get_last_complete_week_range(); $date_from = $date_range['from']; $date_to = $date_range['to']; ob_start(); load_template( SIMPLE_HISTORY_PATH . 'templates/email-summary-report.php', false, $this->get_summary_report_data( $date_from, $date_to, false ) ); $email_content = ob_get_clean(); $subject = $this->get_email_subject( false ); $headers = [ 'Content-Type: text/html; charset=UTF-8' ]; // Send to each recipient. foreach ( $recipients as $recipient ) { // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail -- Not bulk, this is the email that is sent to a short list of manually added recipients. wp_mail( $recipient, $subject, $email_content, $headers ); } } }