|null */ private static ?array $dismissed_cache = null; /** * Hook suffix assigned by add_submenu_page(). * * @var string */ private string $hook_suffix = ''; // ------------------------------------------------------------------------- // Public static data methods // ------------------------------------------------------------------------- /** * Returns all dismissed attachment IDs as a map keyed by attachment_id. * * Cached for the lifetime of the current request. * * @return array */ public static function get_dismissed_ids(): array { if ( null !== self::$dismissed_cache ) { return self::$dismissed_cache; } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_col( "SELECT attachment_id FROM {$wpdb->prefix}mra_dismissed_alerts" ); self::$dismissed_cache = array(); if ( is_array( $rows ) ) { foreach ( $rows as $id ) { if ( is_numeric( $id ) ) { self::$dismissed_cache[ (int) $id ] = true; } } } return self::$dismissed_cache; } /** * Invalidates the in-request dismissed-IDs cache. * * @return void */ public static function invalidate_cache(): void { self::$dismissed_cache = null; } /** * Inserts or replaces a dismissal record for an attachment. * * @param int $attachment_id WordPress attachment post ID. * @param int $user_id WordPress user ID performing the dismissal. * @param string $notes Optional notes (max NOTES_MAX chars). * * @return void */ public static function dismiss( int $attachment_id, int $user_id, string $notes ): void { global $wpdb; $notes = mb_substr( $notes, 0, self::NOTES_MAX ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->query( $wpdb->prepare( "INSERT INTO {$wpdb->prefix}mra_dismissed_alerts (attachment_id, dismissed_by, dismissed_at, notes) VALUES (%d, %d, %s, %s) ON DUPLICATE KEY UPDATE dismissed_by = VALUES(dismissed_by), dismissed_at = VALUES(dismissed_at), notes = VALUES(notes)", $attachment_id, $user_id, current_time( 'mysql' ), $notes ) ); self::invalidate_cache(); } /** * Removes a dismissal record, making the alert active again. * * @param int $attachment_id WordPress attachment post ID. * * @return void */ public static function reactivate( int $attachment_id ): void { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->delete( $wpdb->prefix . 'mra_dismissed_alerts', array( 'attachment_id' => $attachment_id ), array( '%d' ) ); self::invalidate_cache(); } // ------------------------------------------------------------------------- // Instance methods // ------------------------------------------------------------------------- /** * Stores the hook suffix and registers the load-* action. * * @param string $suffix Hook suffix from add_submenu_page(). * * @return void */ public function set_hook_suffix( string $suffix ): void { $this->hook_suffix = $suffix; add_action( 'load-' . $suffix, array( $this, 'handle_load' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); } /** * Enqueues CSS and JS assets for the Alerts page. * * @param string $hook Current admin page hook suffix. * * @return void */ public function enqueue_assets( string $hook ): void { if ( '' === $this->hook_suffix || $hook !== $this->hook_suffix ) { return; } $base = ROBOTSTXT_MEDIAAUDIT_PLUGIN_URL . 'assets/'; $ver = ROBOTSTXT_MEDIAAUDIT_VERSION; wp_enqueue_style( 'mra-admin', $base . 'css/media-audit-admin.css', array(), $ver ); wp_enqueue_script( 'mra-admin', $base . 'js/media-audit-admin.js', array( 'jquery' ), $ver, true ); } /** * Fires before the admin page HTML is output. * * Processes dismiss / reactivate POST actions and issues a PRG redirect. * * @return void */ public function handle_load(): void { if ( ! isset( $_POST[ self::NONCE_FIELD ] ) ) { return; } if ( ! current_user_can( 'edit_others_posts' ) ) { wp_die( esc_html__( 'You do not have permission to perform this action.', 'robotstxt-mediaaudit' ) ); } check_admin_referer( self::NONCE_ACTION, self::NONCE_FIELD ); $op_raw = isset( $_POST['mra_alert_op'] ) && is_string( $_POST['mra_alert_op'] ) ? sanitize_key( wp_unslash( $_POST['mra_alert_op'] ) ) : ''; $aid_raw = isset( $_POST['mra_attachment_id'] ) && is_string( $_POST['mra_attachment_id'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['mra_attachment_id'] ) ) : 0; $notice = 'error'; if ( $aid_raw > 0 ) { if ( 'dismiss' === $op_raw ) { $notes_raw = isset( $_POST['mra_notes'] ) && is_string( $_POST['mra_notes'] ) ? sanitize_textarea_field( wp_unslash( $_POST['mra_notes'] ) ) : ''; self::dismiss( $aid_raw, get_current_user_id(), $notes_raw ); $notice = 'dismissed'; } elseif ( 'reactivate' === $op_raw ) { self::reactivate( $aid_raw ); $notice = 'reactivated'; } } // Read tab from POST body (the hidden input in the form), fall back to GET. $tab_raw = isset( $_POST['mra_tab'] ) && is_string( $_POST['mra_tab'] ) ? sanitize_key( wp_unslash( $_POST['mra_tab'] ) ) : ''; $tab = in_array( $tab_raw, array( 'active', 'dismissed' ), true ) ? $tab_raw : $this->get_active_tab(); wp_safe_redirect( add_query_arg( array( 'page' => 'robotstxt-mediaaudit-alerts', 'tab' => $tab, 'mra_notice' => $notice, 'mra_op' => $op_raw, ), admin_url( 'admin.php' ) ) ); exit; } /** * Renders the Alerts admin page. * * @return void */ public function render(): void { if ( ! current_user_can( 'edit_others_posts' ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'robotstxt-mediaaudit' ) ); } $active_rows = self::get_active_alert_rows(); $dismissed_rows = self::get_dismissed_rows(); $tab = $this->get_active_tab(); $notice = $this->get_notice_data(); $active_count = count( $active_rows ); $dismissed_count = count( $dismissed_rows ); $paged = max( 1, isset( $_GET['paged'] ) && is_numeric( $_GET['paged'] ) ? (int) $_GET['paged'] : 1 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $offset = ( $paged - 1 ) * self::PER_PAGE; $page_url = add_query_arg( 'page', 'robotstxt-mediaaudit-alerts', admin_url( 'admin.php' ) ); ?>

render_notice( $notice ); ?> render_active_tab( $page_rows, $active_count, $paged, $page_url ); ?> render_dismissed_tab( $page_rows, $dismissed_count, $paged, $page_url ); ?>
$type, 'op' => $op, ); } /** * Renders a WP admin notice based on the PRG redirect result. * * @param array{type: string, op: string}|null $notice Notice data array, or null. * * @return void */ private function render_notice( ?array $notice ): void { if ( null === $notice ) { return; } $type = $notice['type']; $messages = array( 'dismissed' => __( 'Alert dismissed successfully.', 'robotstxt-mediaaudit' ), 'reactivated' => __( 'Alert reactivated successfully.', 'robotstxt-mediaaudit' ), 'error' => __( 'An error occurred. Please try again.', 'robotstxt-mediaaudit' ), ); $class = ( 'error' === $type ) ? 'notice-error' : 'notice-success'; $msg = $messages[ $type ] ?? __( 'Operation completed.', 'robotstxt-mediaaudit' ); ?>

> $rows Page rows. * @param int $total Total row count. * @param int $paged Current page number. * @param string $page_url Base page URL. * * @return void */ private function render_active_tab( array $rows, int $total, int $paged, string $page_url ): void { if ( empty( $rows ) ) { echo '

' . esc_html__( 'No active alerts found.', 'robotstxt-mediaaudit' ) . '

'; return; } ?> $row ) : ?> 'mra-thumb' ) ); if ( ! $thumb ) { $thumb = ''; } ?>
' . esc_html__( 'Edit', 'robotstxt-mediaaudit' ) . ''; } $row_links[] = '' . esc_html__( 'Full Report', 'robotstxt-mediaaudit' ) . ''; echo implode( ' | ', $row_links ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
    $count ) : ?>
  • ()
add_query_arg( 'paged', '%#%', add_query_arg( 'tab', 'active', $page_url ) ), 'format' => '', 'current' => $paged, 'total' => (int) ceil( $total / self::PER_PAGE ), 'prev_text' => '«', 'next_text' => '»', ) ); if ( $pagination ) { echo '
' . $pagination . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } /** * Renders the Dismissed Alerts tab. * * @param array> $rows Page rows. * @param int $total Total row count. * @param int $paged Current page number. * @param string $page_url Base page URL. * * @return void */ private function render_dismissed_tab( array $rows, int $total, int $paged, string $page_url ): void { if ( empty( $rows ) ) { echo '

' . esc_html__( 'No dismissed alerts.', 'robotstxt-mediaaudit' ) . '

'; return; } ?> 0 ? get_user_by( 'id', $dismissed_by ) : false; $display_name = ( $user && isset( $user->display_name ) ) ? $user->display_name : (string) $dismissed_by; $thumb = wp_get_attachment_image( $aid, array( 40, 40 ), false, array( 'class' => 'mra-thumb' ) ); if ( ! $thumb ) { $thumb = ''; } ?>
' . esc_html__( 'Edit', 'robotstxt-mediaaudit' ) . ''; } $row_links2[] = '' . esc_html__( 'Full Report', 'robotstxt-mediaaudit' ) . ''; echo implode( ' | ', $row_links2 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
    $count ) : ?>
  • ()
add_query_arg( 'paged', '%#%', add_query_arg( 'tab', 'dismissed', $page_url ) ), 'format' => '', 'current' => $paged, 'total' => (int) ceil( $total / self::PER_PAGE ), 'prev_text' => '«', 'next_text' => '»', ) ); if ( $pagination ) { echo '
' . $pagination . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } // ------------------------------------------------------------------------- // Private static data methods // ------------------------------------------------------------------------- /** * Queries active (non-dismissed) alert rows. * * Fetches all mra_media_index candidates that have at least one row in * mra_external_results (via EXISTS), excludes dismissed attachments via LEFT JOIN, * bulk-fetches top_domains, and PHP-filters to only rows with at least one alert domain. * * @return array> */ private static function get_active_alert_rows(): array { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared $candidates = $wpdb->get_results( "SELECT i.attachment_id, i.file_name, i.file_url, (SELECT COUNT(*) FROM {$wpdb->prefix}mra_media_usage mu WHERE mu.attachment_id = i.attachment_id) AS usage_count FROM {$wpdb->prefix}mra_media_index i LEFT JOIN {$wpdb->prefix}mra_dismissed_alerts da ON da.attachment_id = i.attachment_id WHERE da.attachment_id IS NULL AND EXISTS (SELECT 1 FROM {$wpdb->prefix}mra_external_results er WHERE er.attachment_id = i.attachment_id) ORDER BY i.attachment_id ASC", ARRAY_A ); // phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared if ( ! is_array( $candidates ) || empty( $candidates ) ) { return array(); } $all_ids = array_map( 'intval', array_column( $candidates, 'attachment_id' ) ); $top_domains_by_id = MediaListTable::fetch_top_domains( $all_ids ); $result = array(); foreach ( $candidates as $row ) { if ( ! is_array( $row ) ) { continue; } $aid_val = $row['attachment_id'] ?? null; $aid = is_numeric( $aid_val ) ? (int) $aid_val : 0; $domains = $top_domains_by_id[ $aid ] ?? array(); // Keep only alert-classified domains. $alert_domains = array(); foreach ( $domains as $domain => $count ) { if ( HostnameFilter::classify( $domain ) === 'alert' ) { $alert_domains[ $domain ] = $count; } } if ( empty( $alert_domains ) ) { continue; } $result[] = array( 'attachment_id' => $aid, 'file_name' => is_string( $row['file_name'] ?? null ) ? $row['file_name'] : '', 'file_url' => is_string( $row['file_url'] ?? null ) ? $row['file_url'] : '', 'usage_count' => is_numeric( $row['usage_count'] ?? null ) ? (int) $row['usage_count'] : 0, 'top_domains_data' => $alert_domains, ); } return $result; } /** * Queries dismissed alert rows. * * Fetches all mra_dismissed_alerts records LEFT JOINed with mra_media_index, * bulk-fetches top_domains, and PHP-filters to alert domains only. * * @return array> */ private static function get_dismissed_rows(): array { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared $candidates = $wpdb->get_results( "SELECT da.attachment_id, da.dismissed_by, da.dismissed_at, da.notes, COALESCE(i.file_name, '') AS file_name, COALESCE(i.file_url, '') AS file_url FROM {$wpdb->prefix}mra_dismissed_alerts da LEFT JOIN {$wpdb->prefix}mra_media_index i ON i.attachment_id = da.attachment_id ORDER BY da.dismissed_at DESC", ARRAY_A ); // phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared if ( ! is_array( $candidates ) || empty( $candidates ) ) { return array(); } $all_ids = array_map( 'intval', array_column( $candidates, 'attachment_id' ) ); $top_domains_by_id = MediaListTable::fetch_top_domains( $all_ids ); $result = array(); foreach ( $candidates as $row ) { if ( ! is_array( $row ) ) { continue; } $aid_val = $row['attachment_id'] ?? null; $aid = is_numeric( $aid_val ) ? (int) $aid_val : 0; $domains = $top_domains_by_id[ $aid ] ?? array(); // Keep only alert-classified domains. $alert_domains = array(); foreach ( $domains as $domain => $count ) { if ( HostnameFilter::classify( $domain ) === 'alert' ) { $alert_domains[ $domain ] = $count; } } $result[] = array( 'attachment_id' => $aid, 'file_name' => is_string( $row['file_name'] ?? null ) ? $row['file_name'] : '', 'file_url' => is_string( $row['file_url'] ?? null ) ? $row['file_url'] : '', 'dismissed_by' => is_numeric( $row['dismissed_by'] ?? null ) ? (int) $row['dismissed_by'] : 0, 'dismissed_at' => is_string( $row['dismissed_at'] ?? null ) ? $row['dismissed_at'] : '', 'notes' => is_string( $row['notes'] ?? null ) ? $row['notes'] : '', 'top_domains_data' => $alert_domains, ); } return $result; } }