add_hooks(); } public static function init() { if ( self::$instance ) { return; } self::$instance = new self(); } private function add_hooks() { add_filter( 'user_has_cap', [ $this, 'add_cap_dynamically' ], 10, 4 ); add_action( 'init', array( $this, 'redirect_from_grading' ) ); add_action( 'wp_ajax_itsec_grade_report_page', array( $this, 'handle_ajax_request' ) ); add_filter( 'itsec-admin-page-file-path-grade-report', array( $this, 'get_admin_page_file' ) ); add_filter( 'itsec-admin-page-refs', array( $this, 'filter_admin_page_refs' ), 10, 4 ); add_filter( 'itsec_notifications', array( $this, 'register_notification' ) ); add_filter( 'itsec_grade-report-change_notification_strings', array( $this, 'notification_strings' ) ); add_action( 'itsec_scheduled_check-grade-report', array( $this, 'check_grade_report' ) ); add_action( 'itsec_grade_report_changed', array( $this, 'grade_report_changed' ), 10, 2 ); add_action( 'itsec_scheduled_send-grade-report', array( $this, 'maybe_send_grade_report' ) ); add_filter( 'itsec_mail_digest', array( $this, 'customize_digest' ), 10, 3 ); } /** * Dynamically add the grade report capability if the user is in the required user group. * * @param array $allcaps * @param array $requested_caps * @param array $args * @param WP_User $user * * @return array */ public function add_cap_dynamically( $allcaps, $requested_caps, $args, $user ) { if ( ! in_array( self::VIEW_CAP, $requested_caps, true ) ) { return $allcaps; } if ( isset( $allcaps[ self::VIEW_CAP ] ) ) { return $allcaps; } if ( $this->can_user_access( $user ) ) { $allcaps[ self::VIEW_CAP ] = true; } return $allcaps; } public function redirect_from_grading() { if ( is_admin() && isset( $_GET['page'] ) && 'itsec-grade-report' === $_GET['page'] ) { if ( ITSEC_Core::current_user_can_manage() && ! $this->can_user_access() ) { wp_redirect( ITSEC_Core::get_settings_page_url() ); die; } } } public function get_admin_page_file( $file ) { return dirname( __FILE__ ) . '/admin-page/page.php'; } public function filter_admin_page_refs( $page_refs, $capability, $callback, $parent ) { if ( $this->can_user_access() && ITSEC_Core::is_onboarded() ) { $page_refs[] = add_submenu_page( $parent, '', __( 'Grade Report', 'it-l10n-ithemes-security-pro' ), self::VIEW_CAP, 'itsec-grade-report', $callback ); } return $page_refs; } public function handle_ajax_request() { do_action( 'wp_ajax_itsec_settings_page' ); } /** * Register the "Grade Report Change" notification. * * @param array $notifications * * @return array */ public function register_notification( $notifications ) { $notifications['grade-report-change'] = array( 'subject_editable' => true, 'recipient' => ITSEC_Notification_Center::R_USER_LIST, 'schedule' => array( 'min' => ITSEC_Notification_Center::S_DAILY, 'max' => ITSEC_Notification_Center::S_WEEKLY, 'setting_only' => true ), 'optional' => true, ); return $notifications; } /** * Get the strings for the "Grade Report Change" notification. * * @return array */ public function notification_strings() { return array( 'label' => __( 'Grade Report Change', 'it-l10n-ithemes-security-pro' ), 'description' => __( 'Receive a notification whenever your Security Grade Report changes.', 'it-l10n-ithemes-security-pro' ), 'subject' => __( 'Your Security Grade has Changed', 'it-l10n-ithemes-security-pro' ), ); } /** * Check whether a grade report has changed since the last check. */ public function check_grade_report() { require_once( dirname( __FILE__ ) . '/report.php' ); $previous = get_site_option( 'itsec_last_grade_report', array() ); $current = ITSEC_Grading_System::get_report(); if ( ! $previous ) { update_site_option( 'itsec_last_grade_report', $current ); return; } if ( $previous['hash'] !== $current['hash'] ) { /** * Fires when the grade report has changed. * * @param array $current * @param array $previous */ do_action( 'itsec_grade_report_changed', $current, $previous ); update_site_option( 'itsec_last_grade_report', $current ); } } /** * When the grade report changes, schedule an event to send a notification. * * @param array $report * @param array $previous */ public function grade_report_changed( $report, $previous ) { if ( ! ITSEC_Core::get_notification_center()->is_notification_enabled( 'grade-report-change' ) ) { return; } if ( ! ITSEC_Core::get_scheduler()->is_single_scheduled( 'send-grade-report', null ) ) { $at = ITSEC_Core::get_current_time_gmt() + HOUR_IN_SECONDS; ITSEC_Core::get_scheduler()->schedule_once( $at, 'send-grade-report', compact( 'report', 'previous', 'at' ) ); } } /** * Send the grade report email if the report hasn't changed. * * @param ITSEC_Job $job */ public function maybe_send_grade_report( $job ) { switch ( ITSEC_Core::get_notification_center()->get_schedule( 'grade-report-change' ) ) { case ITSEC_Notification_Center::S_WEEKLY: $delay = WEEK_IN_SECONDS; break; case ITSEC_Notification_Center::S_DAILY: default: $delay = DAY_IN_SECONDS; } $data = $job->get_data(); require_once( dirname( __FILE__ ) . '/report.php' ); $report_now = ITSEC_Grading_System::get_report(); if ( $report_now['hash'] === $data['previous']['hash'] ) { return; } // If we've been trying to send this report for 24 hours, then just send it. if ( $data['at'] + $delay < ITSEC_Core::get_current_time_gmt() ) { $this->send_grade_report( $report_now, $data['previous'] ); return; } // If the report is different from when we were trying to send it, then assume // that the report is being actively effected and wait until it calms down. if ( $report_now['hash'] !== $data['report']['hash'] ) { $job->reschedule_in( HOUR_IN_SECONDS, array( 'report' => $report_now ) ); return; } $last_sent = ITSEC_Core::get_notification_center()->get_last_sent( 'grade-report-change' ); $not_before = $last_sent + $delay; // If the not before date is in the future, then schedule the change event for that time. if ( $not_before > ITSEC_Core::get_current_time_gmt() ) { $job->reschedule_in( $not_before - ITSEC_Core::get_current_time_gmt() ); return; } $this->send_grade_report( $report_now, $data['previous'] ); } /** * Send the Grade Report to the user. * * @param array $report * @param array $previous */ private function send_grade_report( $report, $previous ) { $nc = ITSEC_Core::get_notification_center(); if ( ! $nc->is_notification_enabled( 'grade-report-change' ) ) { return; } $mail = $nc->mail(); $mail->add_header( __( 'Grade Report Update', 'it-l10n-ithemes-security-pro' ), __( 'Update: Your Security Grade has changed', 'it-l10n-ithemes-security-pro' ) ); $mail->add_html( $this->get_grade_section_html( $mail, $report ), 'grade-summary' ); $issues_table_header = array( esc_html__( 'Issue', 'it-l10n-ithemes-security-pro' ), esc_html__( 'Grade', 'it-l10n-ithemes-security-pro' ), esc_html__( 'Description', 'it-l10n-ithemes-security-pro' ) ); $issues = $this->get_issues( $report, $previous ); if ( $issues['new'] ) { $mail->add_large_text( esc_html__( 'New Issues That Need Your Attention', 'it-l10n-ithemes-security-pro' ) ); $table = array(); foreach ( $issues['new'] as $issue ) { $table[] = array( $issue['name'], $issue['grade'], $issue['details'] ); } $mail->add_table( $issues_table_header, $table ); } if ( $issues['existing'] ) { $mail->add_large_text( esc_html__( 'Existing Issues Impacting Your Grade', 'it-l10n-ithemes-security-pro' ) ); $table = array(); foreach ( $issues['existing'] as $issue ) { $table[] = array( $issue['name'], $issue['grade'], $issue['details'] ); } $mail->add_table( $issues_table_header, $table ); } if ( ! $issues['new'] && ! $issues['existing'] ) { return; } $mail->add_footer(); $nc->send( 'grade-report-change', $mail ); $nc->update_last_sent( 'grade-report-change' ); } private function get_issues( $report, $previous ) { $new = $existing = array(); foreach ( $report['sections'] as $i => $section ) { foreach ( $section['criteria'] as $id => $criterion ) { if ( ! $criterion['issue'] ) { continue; } if ( ! isset( $previous['sections'][ $i ]['criteria'][ $id ]['percent'] ) ) { $new[] = $criterion; } elseif ( $criterion['percent'] !== $previous['sections'][ $i ]['criteria'][ $id ]['percent'] ) { $new[] = $criterion; } else { $existing[] = $criterion; } } } return compact( 'new', 'existing' ); } /** * Customize the Daily Digest email to include the current grade. * * @param array $content * @param ITSEC_Mail $mail * @param string $recipient * * @return array */ public function customize_digest( $content, $mail, $recipient ) { if ( ! isset( $content['intro'] ) ) { return $content; } if ( $recipient && ( $user = get_user_by( 'email', $recipient ) ) && ! $this->can_user_access( $user ) ) { return $content; } require_once( dirname( __FILE__ ) . '/report.php' ); $report = ITSEC_Grading_System::get_report(); $summary = $this->get_grade_section_html( $mail, $report ); $content = ITSEC_Lib::array_insert_after( 'intro', $content, 'grade-summary', $summary ); return $content; } /** * Get the grade overview HTML. * * @param ITSEC_Mail $mail * @param array $report * * @return string */ private function get_grade_section_html( $mail, $report ) { $grade = $report['grade']['real']; switch ( $grade[0] ) { case 'A': $color = '#00C778'; break; case 'B': $color = '#00A0D2'; break; case 'C': $color = '#FA9408'; break; case 'D': $color = '#E7635D'; break; case 'F': $color = '#98030E'; break; default: $color = ''; break; } return $this->get_grade_summary_html( $grade, $color, $this->get_summary( $report ) ) . $mail->get_divider(); } private function get_grade_summary_html( $grade, $color, $summary ) { $template = file_get_contents( dirname( __FILE__ ) . '/mail-templates/grade-overview.html' ); $tags = array( 'grade' => $grade, 'grade_color' => $color, 'summary' => $summary, 'title' => esc_html__( 'Your Current WordPress Security Grade', 'it-l10n-ithemes-security-pro' ), 'button_text' => esc_html__( 'See Your Grade Report →', 'it-l10n-ithemes-security-pro' ), 'button_link' => esc_url( network_admin_url( 'admin.php?page=itsec-grade-report' ) ), ); return ITSEC_Lib::replace_tags( $template, $tags ); } private function get_summary( $report ) { if ( 0 === $report['issues'] ) { return esc_html__( 'Great work! Based on your current security settings and software, your website has gotten the top WordPress security grade possible.', 'it-l10n-ithemes-security-pro' ); } if ( 0 === $report['fixable_issues'] ) { return sprintf( esc_html__( 'Your WordPress Security Grade is based on your current security settings and software. %1$sView details%2$s about your WordPress security grade.', 'it-l10n-ithemes-security-pro' ), '', '' ); } return sprintf( esc_html__( 'Your WordPress Security Grade is based on your current security settings and software. Resolve %1$sthese issues now%2$s to raise your WordPress security grade.', 'it-l10n-ithemes-security-pro' ), '', '' ); } private function can_user_access( WP_User $user = null ) { /** @var User_Groups\Matcher $matcher */ $matcher = ITSEC_Modules::get_container()->get( Matcher::class ); $group = ITSEC_Modules::get_setting( 'grade-report', 'group' ); return $matcher->matches( User_Groups\Match_Target::for_user( $user ?: wp_get_current_user() ), $group ); } } ITSEC_Grading_System_Active::init();