v1.7.0
This commit is contained in:
parent
98b1cf0f3b
commit
4402f3570d
20 changed files with 1683 additions and 382 deletions
769
includes/Admin/AlertsPage.php
Normal file
769
includes/Admin/AlertsPage.php
Normal file
|
|
@ -0,0 +1,769 @@
|
|||
<?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' ),
|
||||
$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' ) );
|
||||
?>
|
||||
<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' => '«',
|
||||
'next_text' => '»',
|
||||
)
|
||||
);
|
||||
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' => '«',
|
||||
'next_text' => '»',
|
||||
)
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue