* reason => count). No addresses, pages or form contents are involved. * * @package AccessNow\SpamEater */ namespace AccessNow\SpamEater; defined( 'ABSPATH' ) || exit; /** * Dashboard widget and weekly email. */ final class Dashboard { /** WP-Cron hook for the weekly email. */ const CRON_HOOK = 'accessnow_spam_eater_weekly_summary'; /** * The plugin. * * @var Plugin */ private $plugin; /** * Hook up the widget and the weekly email. * * @param Plugin $plugin The plugin. */ public function __construct( Plugin $plugin ) { $this->plugin = $plugin; add_action( 'wp_dashboard_setup', array( $this, 'add_widget' ) ); add_action( self::CRON_HOOK, array( $this, 'send_weekly_email' ) ); add_action( 'admin_init', array( $this, 'check_schedule' ) ); } /** * Schedule or unschedule the weekly email. * * @param bool $on Whether the weekly email is on. */ public static function schedule_weekly_email( $on ) { $next = wp_next_scheduled( self::CRON_HOOK ); if ( $on && ! $next ) { wp_schedule_event( time() + WEEK_IN_SECONDS, 'weekly', self::CRON_HOOK ); } elseif ( ! $on && $next ) { wp_clear_scheduled_hook( self::CRON_HOOK ); } } /** * Action: admin_init. Put the schedule right after the plugin was deactivated and * activated again (deactivating removes it). */ public function check_schedule() { $settings = get_option( 'accessnow_spam_eater_settings', array() ); self::schedule_weekly_email( is_array( $settings ) && ! empty( $settings['weekly_email'] ) ); } /** * Action: wp_dashboard_setup. Administrators only. */ public function add_widget() { if ( ! current_user_can( 'manage_options' ) ) { return; } wp_add_dashboard_widget( 'accessnow_spam_eater_summary', __( 'Spam Eater: last 30 days', 'accessnow-spam-eater' ), array( $this, 'render_widget' ) ); } /** * The widget's content. */ public function render_widget() { self::render_summary( $this->plugin, 30 ); echo '

' . esc_html__( 'Spam Eater settings', 'accessnow-spam-eater' ) . ' · ' . esc_html__( 'About this summary', 'accessnow-spam-eater' ) . '

'; } /** * Totals by reason and by day for a period. * * @param Plugin $plugin The plugin. * @param int $days Days. * @return array{days:array,reasons:array,total:int} */ public static function totals( Plugin $plugin, $days ) { $by_day = array(); $by_reason = array(); foreach ( $plugin->get_daily_stats( $days ) as $day => $reasons ) { $by_day[ $day ] = 0; foreach ( $reasons as $reason => $count ) { if ( 'login_throttled' === $reason ) { continue; // Not a submission; reported on its own. } $by_day[ $day ] += $count; $by_reason[ $reason ] = ( isset( $by_reason[ $reason ] ) ? $by_reason[ $reason ] : 0 ) + $count; } } arsort( $by_reason ); $throttled = 0; foreach ( $plugin->get_daily_stats( $days ) as $reasons ) { $throttled += isset( $reasons['login_throttled'] ) ? (int) $reasons['login_throttled'] : 0; } return array( 'days' => $by_day, 'reasons' => $by_reason, 'total' => array_sum( $by_day ), 'throttled' => $throttled, ); } /** * Print the summary: a sentence, a bar chart (drawn as SVG, hidden from screen readers) * and the same figures as tables, so nothing is only in the picture. * * @param Plugin $plugin The plugin. * @param int $days Days. */ public static function render_summary( Plugin $plugin, $days ) { $totals = self::totals( $plugin, $days ); $max = max( 1, max( $totals['days'] ) ); $count = count( $totals['days'] ); $width = 300; $height = 80; $slot = $width / max( 1, $count ); $date = get_option( 'date_format' ); echo '
'; echo '

'; printf( /* translators: 1: number of submissions, 2: number of days. */ esc_html( _n( '%1$s spam submission refused in the last %2$s days.', '%1$s spam submissions refused in the last %2$s days.', $totals['total'], 'accessnow-spam-eater' ) ), esc_html( number_format_i18n( $totals['total'] ) ), esc_html( number_format_i18n( $days ) ) ); echo '

'; // The chart. Every figure in it is also in the tables below. // Fixed height, stretched across the width: the bars widen with the screen, and the // date labels are text outside the SVG so they never scale with it. echo ''; echo ''; if ( ! empty( $totals['reasons'] ) ) { echo ''; foreach ( $totals['reasons'] as $reason => $value ) { echo ''; } echo '
' . esc_html__( 'Refused submissions by reason', 'accessnow-spam-eater' ) . '
' . esc_html__( 'Reason', 'accessnow-spam-eater' ) . '' . esc_html__( 'Refused', 'accessnow-spam-eater' ) . '
' . esc_html( $plugin->reason_label( $reason ) ) . '' . esc_html( number_format_i18n( $value ) ) . '
'; } if ( $totals['throttled'] > 0 ) { echo '

'; printf( /* translators: %s: number of times. */ esc_html( _n( 'Repeated failed logins were made to wait %s time.', 'Repeated failed logins were made to wait %s times.', $totals['throttled'], 'accessnow-spam-eater' ) ), esc_html( number_format_i18n( $totals['throttled'] ) ) ); echo '

'; } echo '
' . esc_html__( 'Figures for each day', 'accessnow-spam-eater' ) . ''; echo ''; foreach ( array_reverse( $totals['days'], true ) as $day => $value ) { echo ''; } echo '
' . esc_html__( 'Day', 'accessnow-spam-eater' ) . '' . esc_html__( 'Refused', 'accessnow-spam-eater' ) . '
' . esc_html( wp_date( $date, strtotime( $day . ' 12:00:00' ) ) ) . '' . esc_html( number_format_i18n( $value ) ) . '
'; echo '

' . esc_html__( 'Submissions from addresses that are already blocked are turned away without being counted.', 'accessnow-spam-eater' ) . '

'; echo '
'; } /** * WP-Cron: email the last 7 days to the site's administration address. * * @return bool Whether the email was handed to wp_mail(). */ public function send_weekly_email() { $settings = get_option( 'accessnow_spam_eater_settings', array() ); if ( ! is_array( $settings ) || empty( $settings['weekly_email'] ) ) { return false; } $to = (string) get_option( 'admin_email' ); if ( ! is_email( $to ) ) { return false; } $totals = self::totals( $this->plugin, 7 ); $site = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ); /* translators: 1: site name, 2: number of submissions. */ $subject = sprintf( _n( '[%1$s] Spam Eater: %2$s spam submission refused this week', '[%1$s] Spam Eater: %2$s spam submissions refused this week', $totals['total'], 'accessnow-spam-eater' ), $site, number_format_i18n( $totals['total'] ) ); $lines = array(); $lines[] = sprintf( /* translators: %s: number of submissions. */ _n( 'In the last 7 days Spam Eater refused %s spam submission on your site.', 'In the last 7 days Spam Eater refused %s spam submissions on your site.', $totals['total'], 'accessnow-spam-eater' ), number_format_i18n( $totals['total'] ) ); if ( ! empty( $totals['reasons'] ) ) { $lines[] = ''; $lines[] = __( 'Why they were refused:', 'accessnow-spam-eater' ); foreach ( $totals['reasons'] as $reason => $value ) { $lines[] = '- ' . $this->plugin->reason_label( $reason ) . ': ' . number_format_i18n( $value ); } } if ( $totals['throttled'] > 0 ) { $lines[] = ''; /* translators: %s: number of times. */ $lines[] = sprintf( _n( 'Repeated failed logins were made to wait %s time.', 'Repeated failed logins were made to wait %s times.', $totals['throttled'], 'accessnow-spam-eater' ), number_format_i18n( $totals['throttled'] ) ); } $lines[] = ''; $lines[] = __( 'Day by day:', 'accessnow-spam-eater' ); foreach ( $totals['days'] as $day => $value ) { $lines[] = wp_date( 'D j M', strtotime( $day . ' 12:00:00' ) ) . ': ' . number_format_i18n( $value ); } $lines[] = ''; $lines[] = __( 'Counts only: no addresses or form contents are kept for this summary.', 'accessnow-spam-eater' ); /* translators: %s: settings page address. */ $lines[] = sprintf( __( 'Turn this email off under Settings → Spam Eater → Summary and testing: %s', 'accessnow-spam-eater' ), admin_url( 'options-general.php?page=accessnow-spam-eater' ) ); return wp_mail( $to, $subject, implode( "\n", $lines ) ); } }