Brachmelder. * * @package Brachmelder */ namespace Brachmelder\Admin; use Brachmelder\Check\PhpSupport; use Brachmelder\Consent; use Brachmelder\Check\Status; use Brachmelder\Messages; use Brachmelder\Plugin; use Brachmelder\Report; use Brachmelder\ResultStore; use Brachmelder\Scanner; defined( 'ABSPATH' ) || exit; /** * Admin page with the traffic light overview. */ class Page { const SLUG = 'brachmelder'; const CHECK_ACTION = 'brachmelder_check_now'; const ENABLE_ACTION = 'brachmelder_enable'; const DISABLE_ACTION = 'brachmelder_disable'; const FLASH_META = 'brachmelder_flash'; const HELP_URL = 'https://thorstenkoetzsch.de/brachmelder/'; const PRIVACY_URL = 'https://wordpress.org/about/privacy/'; /** * Scanner. * * @var Scanner */ private $scanner; /** * Result storage. * * @var ResultStore */ private $store; /** * Report. * * @var Report */ private $report; /** * Consent to contact WordPress.org. * * @var Consent */ private $consent; /** * Constructor. * * @param Scanner $scanner Scanner. * @param ResultStore $store Result storage. * @param Report $report Report. * @param Consent $consent Consent to contact WordPress.org. */ public function __construct( Scanner $scanner, ResultStore $store, Report $report, Consent $consent ) { $this->scanner = $scanner; $this->store = $store; $this->report = $report; $this->consent = $consent; } /** * Registers hooks. */ public function register(): void { add_action( 'admin_menu', array( $this, 'add_page' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); add_action( 'admin_post_' . self::CHECK_ACTION, array( $this, 'handle_check' ) ); add_action( 'admin_post_' . self::ENABLE_ACTION, array( $this, 'handle_enable' ) ); add_action( 'admin_post_' . self::DISABLE_ACTION, array( $this, 'handle_disable' ) ); add_filter( 'plugin_action_links_' . plugin_basename( BRACHMELDER_FILE ), array( $this, 'action_links' ) ); } /** * URL of the page. * * @return string */ public static function url(): string { return add_query_arg( 'page', self::SLUG, admin_url( 'tools.php' ) ); } /** * HTML id for a plugin row. * * @param string $plugin_file Plugin basename. * @return string */ public static function anchor( string $plugin_file ): string { return 'brachmelder-' . substr( md5( $plugin_file ), 0, 12 ); } /** * Adds Tools > Brachmelder. */ public function add_page(): void { add_management_page( __( 'Brachmelder', 'brachmelder' ), __( 'Brachmelder', 'brachmelder' ), Plugin::CAPABILITY, self::SLUG, array( $this, 'render' ) ); } /** * Adds a link to the page in the plugin list. * * @param array $links Action links. * @return array */ public function action_links( $links ) { array_unshift( $links, sprintf( '%s', esc_url( self::url() ), esc_html__( 'Overview', 'brachmelder' ) ) ); return $links; } /** * Loads the stylesheet on our page and the plugin list. * * @param string $hook_suffix Current admin page. */ public function enqueue( $hook_suffix ): void { if ( in_array( $hook_suffix, array( 'tools_page_' . self::SLUG, 'plugins.php' ), true ) ) { wp_enqueue_style( 'brachmelder-admin', BRACHMELDER_URL . 'assets/css/admin.css', array( 'dashicons' ), BRACHMELDER_VERSION ); } } /** * Handles the "Enable check" button: stores the consent and runs the first check. */ public function handle_enable(): void { if ( ! current_user_can( Consent::CAPABILITY ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'brachmelder' ), 403 ); } check_admin_referer( self::ENABLE_ACTION ); delete_option( Plugin::INTRO_OPTION ); $this->scanner->enable( get_current_user_id() ); $this->run_check(); } /** * Handles the "Disable check" button: withdraws the consent and deletes the results. */ public function handle_disable(): void { if ( ! current_user_can( Consent::CAPABILITY ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'brachmelder' ), 403 ); } check_admin_referer( self::DISABLE_ACTION ); $this->scanner->disable(); update_user_meta( get_current_user_id(), self::FLASH_META, 'disabled' ); wp_safe_redirect( self::url() ); exit; } /** * Handles the "Check now" button. */ public function handle_check(): void { if ( ! current_user_can( Plugin::CAPABILITY ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'brachmelder' ), 403 ); } check_admin_referer( self::CHECK_ACTION ); if ( ! $this->consent->granted() ) { wp_safe_redirect( self::url() ); exit; } $this->run_check(); } /** * Runs a check, remembers the outcome for the next page view and redirects back. */ private function run_check(): void { $started = time(); $remaining = $this->scanner->run_now(); $error = $this->store->get_error(); if ( null !== $error && $error['time'] >= $started ) { $flash = 'unreachable'; } elseif ( $remaining > 0 ) { $flash = 'continues'; } else { $flash = 'done'; } update_user_meta( get_current_user_id(), self::FLASH_META, $flash ); wp_safe_redirect( self::url() ); exit; } /** * Renders the page. */ public function render(): void { if ( ! current_user_can( Plugin::CAPABILITY ) ) { return; } // The note after activation has done its job once the page was opened. if ( current_user_can( Consent::CAPABILITY ) ) { delete_option( Plugin::INTRO_OPTION ); } if ( ! $this->consent->granted() ) { $this->render_without_consent(); return; } $rows = $this->report->rows(); $results = $this->store->get_results(); ?>

render_flash(); ?> render_status_line( $results ); ?> render_summary( $rows ); ?> render_php(); ?> render_attention( $rows ); ?> render_ok( $rows ); ?> render_unknown( $rows ); ?> render_disable(); ?> render_help(); ?>

render_flash(); ?> render_php(); ?> render_help(); ?>

array( 'success', __( 'Check complete.', 'brachmelder' ) ), 'continues' => array( 'info', __( 'The first plugins have been checked. The check continues in the background; reload this page in a few minutes.', 'brachmelder' ) ), 'unreachable' => array( 'warning', __( 'WordPress.org could not be reached. The results shown are from the last successful check. Please try again later.', 'brachmelder' ) ), 'disabled' => array( 'success', __( 'The plugin check is disabled. Brachmelder no longer contacts WordPress.org, and the stored results were deleted.', 'brachmelder' ) ), ); if ( ! isset( $messages[ $flash ] ) ) { return; } printf( '

%2$s

', esc_attr( $messages[ $flash ][0] ), esc_html( $messages[ $flash ][1] ) ); } /** * Last check, running scan, connection error and the button. * * @param array|null $results Stored results. */ private function render_status_line( ?array $results ): void { $scan = $this->store->get_scan(); $error = $this->store->get_error(); ?>

> $rows Rows. */ private function render_summary( array $rows ): void { $counts = Report::count_lights( $rows ); $cards = array( Status::RED => __( 'Act now', 'brachmelder' ), Status::YELLOW => __( 'Keep an eye on', 'brachmelder' ), Status::GREEN => __( 'All good', 'brachmelder' ), Status::GREY => __( 'Not checkable', 'brachmelder' ), ); ?>

    $label ) : ?>
Status::RED, PhpSupport::ENDING => Status::YELLOW, PhpSupport::SUPPORTED => Status::GREEN, ); $light = $map[ $php['status'] ] ?? Status::GREY; ?>

> $rows Rows. */ private function render_attention( array $rows ): void { $rows = array_filter( $rows, static function ( array $row ): bool { return in_array( $row['light'], array( Status::RED, Status::YELLOW ), true ); } ); ?>

render_plugin_card( $row ); } } /** * Card for one plugin with explanation and alternatives. * * @param array $row Row. */ private function render_plugin_card( array $row ): void { $texts = Messages::explain( $row ); ?>

  • render_facts( $row ); ?>

$row Row. */ private function render_facts( array $row ): void { $date_format = (string) get_option( 'date_format' ); if ( Status::CLOSED === $row['status'] ) { if ( ! empty( $row['closed_date'] ) ) { echo '
  • '; /* translators: %s: date */ echo esc_html( sprintf( __( 'Closed on WordPress.org on %s', 'brachmelder' ), wp_date( $date_format, (int) strtotime( $row['closed_date'] . ' 12:00:00 UTC' ) ) ) ); echo '
  • '; } echo '
  • '; /* translators: %s: reason, e.g. "Security issue" */ echo esc_html( sprintf( __( 'Reason: %s', 'brachmelder' ), Messages::closed_reason( $row ) ) ); echo '
  • '; } if ( ! empty( $row['last_updated'] ) ) { echo '
  • '; echo esc_html( sprintf( /* translators: 1: date, 2: human readable time span, e.g. "3 years" */ __( 'Last update: %1$s (%2$s ago)', 'brachmelder' ), wp_date( $date_format, (int) $row['last_updated'] ), human_time_diff( (int) $row['last_updated'] ) ) ); echo '
  • '; } if ( Status::OUTDATED === $row['status'] || Status::ABANDONED === $row['status'] ) { echo '
  • '; echo esc_html( '' !== $row['tested'] /* translators: %s: WordPress version */ ? sprintf( __( 'Tested up to WordPress %s', 'brachmelder' ), $row['tested'] ) : __( 'No information on tested WordPress versions', 'brachmelder' ) ); echo '
  • '; } if ( ! empty( $row['stale'] ) ) { echo '
  • ' . esc_html__( 'Result from an earlier check', 'brachmelder' ) . '
  • '; } if ( '' !== $row['slug'] ) { printf( '
  • %2$s
  • ', esc_url( 'https://wordpress.org/plugins/' . rawurlencode( (string) $row['slug'] ) . '/' ), esc_html__( 'View on WordPress.org', 'brachmelder' ) ); } } /** * Plugins without problems. * * @param array> $rows Rows. */ private function render_ok( array $rows ): void { $rows = array_filter( $rows, static function ( array $row ): bool { return Status::OK === $row['status']; } ); if ( empty( $rows ) ) { return; } $texts = Messages::explain( array( 'status' => Status::OK ) ); ?>

    render_simple_list( $rows ); ?>
    > $rows Rows. */ private function render_unknown( array $rows ): void { $rows = array_filter( $rows, static function ( array $row ): bool { return Status::GREY === $row['light']; } ); if ( empty( $rows ) ) { return; } $texts = Messages::explain( array( 'status' => Status::UNKNOWN ) ); ?>

    render_simple_list( $rows ); ?>
    > $rows Rows. */ private function render_simple_list( array $rows ): void { ?>