Current File : /home/digitaw/www/wp-content/plugins/simple-history/inc/services/class-email-report-service.php
<?php

namespace Simple_History\Services;

use Simple_History\Simple_History;
use Simple_History\Helpers;
use Simple_History\Events_Stats;
use Simple_History\Date_Helper;

/**
 * Service that handles email reports.
 */
class Email_Report_Service extends Service {
	/**
	 * @inheritdoc
	 */
	public function loaded() {
		// Register settings with priority 10 to ensure it loads before RSS feed (priority 15).
		add_action( 'admin_menu', [ $this, 'register_settings' ], 13 );

		// Add settings fields to general section.
		add_action( 'simple_history/settings_page/general_section_output', [ $this, 'on_general_section_output' ] );

		// 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' );
	}

	/**
	 * 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 ) {
					$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 = isset( $day_names[ $busiest_day_number ] ) ? $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 WordPress core statistics.
		$stats['wordpress_updates'] = $events_stats->get_wordpress_core_updates_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_settings_general' );

		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
					),
				]
			);
		} else {
			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() {
		$settings_general_option_group = Simple_History::SETTINGS_GENERAL_OPTION_GROUP;
		$settings_menu_slug            = Simple_History::SETTINGS_MENU_SLUG;

		// 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' ],
			$settings_menu_slug,
			[
				'callback_last' => [ $this, 'settings_section_output_last' ],
			],
		);

		register_setting(
			$settings_general_option_group,
			'simple_history_email_report_enabled',
			[
				'type'              => 'boolean',
				'default'           => false,
				'sanitize_callback' => 'rest_sanitize_boolean',
			]
		);

		register_setting(
			$settings_general_option_group,
			'simple_history_email_report_recipients',
			[
				'type'              => 'string',
				'default'           => '',
				'sanitize_callback' => [ $this, 'sanitize_email_recipients' ],
			]
		);
	}

	/**
	 * 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>';
	}

	/**
	 * Add settings fields to general section.
	 *
	 * Function fired from action `simple_history/settings_page/general_section_output`.
	 */
	public function on_general_section_output() {
		$settings_menu_slug = Simple_History::SETTINGS_MENU_SLUG;

		add_settings_field(
			'simple_history_email_report_enabled',
			Helpers::get_settings_field_title_output( __( 'Enable', 'simple-history' ), 'mark_email_unread' ),
			[ $this, 'settings_field_enabled' ],
			$settings_menu_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' ],
			$settings_menu_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' ],
			$settings_menu_slug,
			'simple_history_email_report_section'
		);
	}

	/**
	 * 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( '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&#10;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
			);
		}
	}
}