robotstxt-mediaaudit/includes/Admin/AlertsPage.php
2026-06-03 06:31:47 +00:00

782 lines
27 KiB
PHP

<?php
/**
* Admin page controller for the Alerts view.
*
* @package MediaRightsAudit\Admin
*/
namespace MediaRightsAudit\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\External\HostnameFilter;
use MediaRightsAudit\Admin\AttachmentDetailPage;
/**
* Registers and renders the Alerts admin page.
*
* Responsibilities:
* - Display active (non-dismissed) alert rows.
* - Display dismissed alert rows.
* - Handle dismiss / reactivate POST actions via PRG.
* - Provide static helpers for dismissed-ID lookups (cached per request).
*/
class AlertsPage {
/**
* Nonce action for all alert operations.
*/
const NONCE_ACTION = 'mra_alert_action';
/**
* Nonce field name.
*/
const NONCE_FIELD = 'mra_alert_nonce';
/**
* Maximum length of dismissal notes.
*/
const NOTES_MAX = 1000;
/**
* Rows per page for paginated tabs.
*/
const PER_PAGE = 25;
/**
* In-request cache of dismissed attachment IDs.
*
* @var array<int, true>|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<int, true>
*/
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', true ),
$notes
)
);
delete_transient( 'mra_alert_count' );
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' )
);
delete_transient( 'mra_alert_count' );
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 );
wp_localize_script(
'mra-admin',
'mraAlerts',
array(
'i18n' => array(
'dismiss' => __( 'Dismiss', 'robotstxt-mediaaudit' ),
'cancel' => __( 'Cancel', 'robotstxt-mediaaudit' ),
),
)
);
}
/**
* 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' ) );
?>
<div class="wrap">
<h1><?php esc_html_e( 'Alerts', 'robotstxt-mediaaudit' ); ?></h1>
<?php $this->render_notice( $notice ); ?>
<nav class="nav-tab-wrapper">
<a
class="nav-tab<?php echo ( 'active' === $tab ) ? ' nav-tab-active' : ''; ?>"
href="<?php echo esc_url( add_query_arg( 'tab', 'active', $page_url ) ); ?>"
>
<?php
printf(
/* translators: %d: count of active alerts */
esc_html__( 'Active Alerts (%d)', 'robotstxt-mediaaudit' ),
(int) $active_count
);
?>
</a>
<a
class="nav-tab<?php echo ( 'dismissed' === $tab ) ? ' nav-tab-active' : ''; ?>"
href="<?php echo esc_url( add_query_arg( 'tab', 'dismissed', $page_url ) ); ?>"
>
<?php
printf(
/* translators: %d: count of dismissed alerts */
esc_html__( 'Dismissed Alerts (%d)', 'robotstxt-mediaaudit' ),
(int) $dismissed_count
);
?>
</a>
</nav>
<?php if ( 'active' === $tab ) : ?>
<?php
$page_rows = array_slice( $active_rows, $offset, self::PER_PAGE );
$this->render_active_tab( $page_rows, $active_count, $paged, $page_url );
?>
<?php else : ?>
<?php
$page_rows = array_slice( $dismissed_rows, $offset, self::PER_PAGE );
$this->render_dismissed_tab( $page_rows, $dismissed_count, $paged, $page_url );
?>
<?php endif; ?>
</div>
<?php
}
// -------------------------------------------------------------------------
// Private rendering helpers
// -------------------------------------------------------------------------
/**
* Returns the active tab slug ('active' or 'dismissed').
*
* @return string
*/
private function get_active_tab(): string {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$tab = isset( $_GET['tab'] ) && is_string( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'active';
return in_array( $tab, array( 'active', 'dismissed' ), true ) ? $tab : 'active';
}
/**
* Reads PRG query parameters and returns structured notice data.
*
* @return array{type: string, op: string}|null
*/
private function get_notice_data(): ?array {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET['mra_notice'] ) ) {
return null;
}
$type = sanitize_key( wp_unslash( is_string( $_GET['mra_notice'] ) ? $_GET['mra_notice'] : '' ) );
$op = sanitize_key( wp_unslash( isset( $_GET['mra_op'] ) && is_string( $_GET['mra_op'] ) ? $_GET['mra_op'] : '' ) );
// phpcs:enable WordPress.Security.NonceVerification.Recommended
return array(
'type' => $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' );
?>
<div class="notice <?php echo esc_attr( $class ); ?> is-dismissible">
<p><?php echo esc_html( $msg ); ?></p>
</div>
<?php
}
/**
* Renders the Active Alerts tab.
*
* @param array<int, array<string, mixed>> $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 '<p>' . esc_html__( 'No active alerts found.', 'robotstxt-mediaaudit' ) . '</p>';
return;
}
?>
<table class="widefat striped mra-alerts-table">
<thead>
<tr>
<th style="width:50px"><?php esc_html_e( 'Thumbnail', 'robotstxt-mediaaudit' ); ?></th>
<th><?php esc_html_e( 'Filename', 'robotstxt-mediaaudit' ); ?></th>
<th><?php esc_html_e( 'Alert Domains', 'robotstxt-mediaaudit' ); ?></th>
<th style="width:80px"><?php esc_html_e( 'Usage', 'robotstxt-mediaaudit' ); ?></th>
<th style="width:120px"><?php esc_html_e( 'Actions', 'robotstxt-mediaaudit' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $rows as $index => $row ) : ?>
<?php
$aid_val = $row['attachment_id'] ?? 0;
$aid = is_numeric( $aid_val ) ? (int) $aid_val : 0;
$file_name_raw = $row['file_name'] ?? '';
$file_name = is_string( $file_name_raw ) ? $file_name_raw : '';
$file_url_raw = $row['file_url'] ?? '';
$file_url = is_string( $file_url_raw ) ? $file_url_raw : '';
$usage_cnt_raw = $row['usage_count'] ?? 0;
$usage_count = is_numeric( $usage_cnt_raw ) ? (int) $usage_cnt_raw : 0;
$domains_raw = $row['top_domains_data'] ?? array();
$domains = is_array( $domains_raw ) ? $domains_raw : array();
$dismiss_id = 'mra-dismiss-row-' . $aid;
$thumb = wp_get_attachment_image( $aid, array( 40, 40 ), false, array( 'class' => 'mra-thumb' ) );
if ( ! $thumb ) {
$thumb = '<span class="dashicons dashicons-format-image" style="font-size:40px;line-height:1;"></span>';
}
?>
<tr>
<td>
<a href="<?php echo esc_url( $file_url ); ?>" target="_blank"><?php echo $thumb; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></a>
</td>
<td>
<a href="<?php echo esc_url( $file_url ); ?>" target="_blank"><?php echo esc_html( '' !== $file_name ? $file_name : '—' ); ?></a>
<div class="row-actions">
<?php
$edit_link = get_edit_post_link( $aid );
$row_links = array();
if ( $edit_link ) {
$row_links[] = '<a href="' . esc_url( $edit_link ) . '">' . esc_html__( 'Edit', 'robotstxt-mediaaudit' ) . '</a>';
}
$row_links[] = '<a href="' . esc_url( AttachmentDetailPage::url( $aid ) ) . '"><strong>' . esc_html__( 'Full Report', 'robotstxt-mediaaudit' ) . '</strong></a>';
echo implode( ' | ', $row_links ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
?>
</div>
</td>
<td>
<?php if ( ! empty( $domains ) ) : ?>
<ul class="mra-alert-domains">
<?php foreach ( $domains as $domain => $count ) : ?>
<li>
<span class="mra-badge mra-badge-alert"><?php esc_html_e( 'Alert', 'robotstxt-mediaaudit' ); ?></span>
<?php echo esc_html( $domain ); ?>
<small>(<?php echo esc_html( number_format_i18n( is_numeric( $count ) ? (int) $count : 0 ) ); ?>)</small>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</td>
<td><?php echo esc_html( number_format_i18n( $usage_count ) ); ?></td>
<td>
<button
type="button"
class="button mra-dismiss-toggle"
data-target="<?php echo esc_attr( $dismiss_id ); ?>"
><?php esc_html_e( 'Dismiss', 'robotstxt-mediaaudit' ); ?></button>
</td>
</tr>
<tr id="<?php echo esc_attr( $dismiss_id ); ?>" class="mra-dismiss-row" hidden>
<td colspan="5">
<div class="mra-dismiss-form-inner">
<form method="post" action="<?php echo esc_url( admin_url( 'admin.php?page=robotstxt-mediaaudit-alerts' ) ); ?>">
<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_FIELD ); ?>
<input type="hidden" name="mra_alert_op" value="dismiss" />
<input type="hidden" name="mra_attachment_id" value="<?php echo esc_attr( (string) $aid ); ?>" />
<input type="hidden" name="mra_tab" value="active" />
<label for="mra-notes-<?php echo esc_attr( (string) $aid ); ?>">
<?php esc_html_e( 'Notes (optional):', 'robotstxt-mediaaudit' ); ?>
</label>
<textarea
id="mra-notes-<?php echo esc_attr( (string) $aid ); ?>"
name="mra_notes"
rows="2"
maxlength="<?php echo esc_attr( (string) self::NOTES_MAX ); ?>"
class="large-text"
></textarea>
<div style="margin-top:8px">
<button type="submit" class="button button-primary"><?php esc_html_e( 'Confirm Dismiss', 'robotstxt-mediaaudit' ); ?></button>
<button type="button" class="button mra-dismiss-cancel"><?php esc_html_e( 'Cancel', 'robotstxt-mediaaudit' ); ?></button>
</div>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php
$pagination = paginate_links(
array(
'base' => add_query_arg( 'paged', '%#%', add_query_arg( 'tab', 'active', $page_url ) ),
'format' => '',
'current' => $paged,
'total' => (int) ceil( $total / self::PER_PAGE ),
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
)
);
if ( $pagination ) {
echo '<div class="tablenav"><div class="tablenav-pages">' . $pagination . '</div></div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
/**
* Renders the Dismissed Alerts tab.
*
* @param array<int, array<string, mixed>> $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 '<p>' . esc_html__( 'No dismissed alerts.', 'robotstxt-mediaaudit' ) . '</p>';
return;
}
?>
<table class="widefat striped mra-alerts-table">
<thead>
<tr>
<th style="width:50px"><?php esc_html_e( 'Thumbnail', 'robotstxt-mediaaudit' ); ?></th>
<th><?php esc_html_e( 'Filename', 'robotstxt-mediaaudit' ); ?></th>
<th><?php esc_html_e( 'Alert Domains', 'robotstxt-mediaaudit' ); ?></th>
<th><?php esc_html_e( 'Notes', 'robotstxt-mediaaudit' ); ?></th>
<th><?php esc_html_e( 'Dismissed By', 'robotstxt-mediaaudit' ); ?></th>
<th><?php esc_html_e( 'Dismissed At', 'robotstxt-mediaaudit' ); ?></th>
<th style="width:120px"><?php esc_html_e( 'Actions', 'robotstxt-mediaaudit' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $rows as $row ) : ?>
<?php
$aid_val = $row['attachment_id'] ?? 0;
$aid = is_numeric( $aid_val ) ? (int) $aid_val : 0;
$file_name_raw = $row['file_name'] ?? '';
$file_name = is_string( $file_name_raw ) ? $file_name_raw : '';
$file_url_raw = $row['file_url'] ?? '';
$file_url = is_string( $file_url_raw ) ? $file_url_raw : '';
$notes_raw = $row['notes'] ?? '';
$notes = is_string( $notes_raw ) ? $notes_raw : '';
$dis_by_raw = $row['dismissed_by'] ?? 0;
$dismissed_by = is_numeric( $dis_by_raw ) ? (int) $dis_by_raw : 0;
$dis_at_raw = $row['dismissed_at'] ?? '';
$dismissed_at = is_string( $dis_at_raw ) ? $dis_at_raw : '';
$domains_raw = $row['top_domains_data'] ?? array();
$domains = is_array( $domains_raw ) ? $domains_raw : array();
$user = $dismissed_by > 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 = '<span class="dashicons dashicons-format-image" style="font-size:40px;line-height:1;"></span>';
}
?>
<tr>
<td>
<a href="<?php echo esc_url( $file_url ); ?>" target="_blank"><?php echo $thumb; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></a>
</td>
<td>
<a href="<?php echo esc_url( $file_url ); ?>" target="_blank"><?php echo esc_html( '' !== $file_name ? $file_name : '—' ); ?></a>
<div class="row-actions">
<?php
$edit_link = get_edit_post_link( $aid );
$row_links2 = array();
if ( $edit_link ) {
$row_links2[] = '<a href="' . esc_url( $edit_link ) . '">' . esc_html__( 'Edit', 'robotstxt-mediaaudit' ) . '</a>';
}
$row_links2[] = '<a href="' . esc_url( AttachmentDetailPage::url( $aid ) ) . '"><strong>' . esc_html__( 'Full Report', 'robotstxt-mediaaudit' ) . '</strong></a>';
echo implode( ' | ', $row_links2 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
?>
</div>
</td>
<td>
<?php if ( ! empty( $domains ) ) : ?>
<ul class="mra-alert-domains">
<?php foreach ( $domains as $domain => $count ) : ?>
<li>
<span class="mra-badge mra-badge-dismissed"><?php esc_html_e( 'Dismissed', 'robotstxt-mediaaudit' ); ?></span>
<?php echo esc_html( $domain ); ?>
<small>(<?php echo esc_html( number_format_i18n( is_numeric( $count ) ? (int) $count : 0 ) ); ?>)</small>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</td>
<td><?php echo esc_html( $notes ); ?></td>
<td><?php echo esc_html( $display_name ); ?></td>
<td><?php echo esc_html( $dismissed_at ); ?></td>
<td>
<form method="post" action="<?php echo esc_url( admin_url( 'admin.php?page=robotstxt-mediaaudit-alerts' ) ); ?>" style="display:inline">
<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_FIELD ); ?>
<input type="hidden" name="mra_alert_op" value="reactivate" />
<input type="hidden" name="mra_attachment_id" value="<?php echo esc_attr( (string) $aid ); ?>" />
<input type="hidden" name="mra_tab" value="dismissed" />
<button type="submit" class="button"><?php esc_html_e( 'Reactivate', 'robotstxt-mediaaudit' ); ?></button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php
$pagination = paginate_links(
array(
'base' => add_query_arg( 'paged', '%#%', add_query_arg( 'tab', 'dismissed', $page_url ) ),
'format' => '',
'current' => $paged,
'total' => (int) ceil( $total / self::PER_PAGE ),
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
)
);
if ( $pagination ) {
echo '<div class="tablenav"><div class="tablenav-pages">' . $pagination . '</div></div>'; // 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<int, array<string, mixed>>
*/
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<int, array<string, mixed>>
*/
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;
}
}