This commit is contained in:
Javier Casares 2026-06-08 18:01:39 +00:00
commit b6a55a7f04
27 changed files with 2221 additions and 45 deletions

View file

@ -0,0 +1,304 @@
<?php
/**
* WP_List_Table subclass for the cross-site media audit list.
*
* @package MediaRightsAudit\Network
*/
namespace MediaRightsAudit\Network;
use MediaRightsAudit\Core\NetworkDatabase;
use MediaRightsAudit\External\HostnameFilter;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
/**
* Renders the aggregated media list in the network admin Audit page.
*
* Queries {base_prefix}mra_network_index for fast cross-site listing.
* Detailed per-attachment data is accessed on demand via per-site URLs.
* A "Site" column is added to identify which site each attachment belongs to.
*/
class NetworkListTable extends \WP_List_Table {
/**
* Rows per page.
*/
const PER_PAGE = 30;
/**
* Alerts-only filter: when true, shows only has_alert=1 rows.
*
* @var bool
*/
private bool $alerts_only;
/**
* Initialises the list table.
*
* @param bool $alerts_only Whether to filter to alert rows only.
*/
public function __construct( bool $alerts_only = false ) {
parent::__construct(
array(
'singular' => 'mra-network-attachment',
'plural' => 'mra-network-attachments',
'ajax' => false,
)
);
$this->alerts_only = $alerts_only;
}
/**
* Returns the column definitions.
*
* @return array<string, string>
*/
public function get_columns(): array {
return array(
'cb' => '<input type="checkbox" />',
'site' => __( 'Site', 'robotstxt-mediaaudit' ),
'file_name' => __( 'Filename', 'robotstxt-mediaaudit' ),
'external_status' => __( 'External Status', 'robotstxt-mediaaudit' ),
'has_alert' => __( 'Alert', 'robotstxt-mediaaudit' ),
'external_scanned_at' => __( 'Last Scanned', 'robotstxt-mediaaudit' ),
);
}
/**
* Returns sortable column definitions.
*
* @return array<string, array<int, mixed>>
*/
protected function get_sortable_columns(): array {
return array(
'site' => array( 'site_url', false ),
'file_name' => array( 'file_name', false ),
'external_status' => array( 'external_status', false ),
'external_scanned_at' => array( 'external_scanned_at', false ),
);
}
/**
* Returns bulk action definitions.
*
* @return array<string, string>
*/
protected function get_bulk_actions(): array {
return array(
'run_external_scan' => __( 'Run External Scan', 'robotstxt-mediaaudit' ),
'purge_external_data' => __( 'Purge External Data', 'robotstxt-mediaaudit' ),
);
}
/**
* Renders the checkbox column.
*
* @param array<string, mixed> $item Row data.
*
* @return string
*/
protected function column_cb( $item ): string {
$site_id = is_numeric( $item['site_id'] ?? null ) ? (int) $item['site_id'] : 0;
$aid = is_numeric( $item['attachment_id'] ?? null ) ? (int) $item['attachment_id'] : 0;
return sprintf(
'<input type="checkbox" name="mra_item[]" value="%d_%d" />',
$site_id,
$aid
);
}
/**
* Renders the Site column.
*
* @param array<string, mixed> $item Row data.
*
* @return string
*/
protected function column_site( array $item ): string {
$site_id = is_numeric( $item['site_id'] ?? null ) ? (int) $item['site_id'] : 0;
$site_url = is_string( $item['site_url'] ?? null ) ? $item['site_url'] : '';
$site_details = get_site( $site_id );
$site_name = $site_details instanceof \WP_Site ? esc_html( $site_details->blogname ) : esc_html( $site_url );
if ( $site_url ) {
$admin_url = get_admin_url( $site_id, 'admin.php?page=robotstxt-mediaaudit' );
return sprintf(
'<a href="%s" target="_blank">%s</a><br /><small>%s</small>',
esc_url( $admin_url ),
$site_name,
esc_html( $site_url )
);
}
return $site_name;
}
/**
* Renders the Filename column with a link to the per-site detail page.
*
* @param array<string, mixed> $item Row data.
*
* @return string
*/
protected function column_file_name( array $item ): string {
$site_id = is_numeric( $item['site_id'] ?? null ) ? (int) $item['site_id'] : 0;
$aid = is_numeric( $item['attachment_id'] ?? null ) ? (int) $item['attachment_id'] : 0;
$filename = is_string( $item['file_name'] ?? null ) ? $item['file_name'] : "#{$aid}";
$detail_url = get_admin_url(
$site_id,
'admin.php?page=robotstxt-mediaaudit-detail&attachment_id=' . $aid
);
return sprintf(
'<a href="%s" target="_blank">%s</a>',
esc_url( $detail_url ),
esc_html( $filename )
);
}
/**
* Renders the External Status column.
*
* @param array<string, mixed> $item Row data.
*
* @return string
*/
protected function column_external_status( array $item ): string {
$status = is_string( $item['external_status'] ?? null ) ? $item['external_status'] : 'pending';
return '<span class="mra-badge mra-status-' . esc_attr( $status ) . '">' . esc_html( ucfirst( $status ) ) . '</span>';
}
/**
* Renders the Alert column.
*
* @param array<string, mixed> $item Row data.
*
* @return string
*/
protected function column_has_alert( array $item ): string {
$has_alert = ! empty( $item['has_alert'] );
$is_dismissed = ! empty( $item['is_dismissed'] );
if ( $has_alert && $is_dismissed ) {
return '<span class="mra-badge">' . esc_html__( 'Dismissed', 'robotstxt-mediaaudit' ) . '</span>';
}
if ( $has_alert ) {
return '<span class="mra-badge mra-badge-alert">' . esc_html__( 'Alert', 'robotstxt-mediaaudit' ) . '</span>';
}
return '&mdash;';
}
/**
* Renders the Last Scanned column.
*
* @param array<string, mixed> $item Row data.
*
* @return string
*/
protected function column_external_scanned_at( array $item ): string {
$val = $item['external_scanned_at'] ?? null;
return is_string( $val ) && '' !== $val ? esc_html( $val ) : '&mdash;';
}
/**
* Default column renderer.
*
* @param array<string, mixed> $item Row data.
* @param string $column_name Column key.
*
* @return string
*/
protected function column_default( $item, $column_name ): string {
$val = $item[ $column_name ] ?? '';
return is_scalar( $val ) ? esc_html( (string) $val ) : '';
}
/**
* Queries the network mirror table and populates $this->items.
*
* @return void
*/
public function prepare_items(): void {
global $wpdb;
$table = NetworkDatabase::table_name();
$per_pg = self::PER_PAGE;
$current = $this->get_pagenum();
$offset = ( $current - 1 ) * $per_pg;
$where = array( '1=1' );
$args = array();
if ( $this->alerts_only ) {
$where[] = 'has_alert = 1 AND is_dismissed = 0';
}
// Site filter.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$site_filter = isset( $_GET['mra_site'] ) && is_numeric( $_GET['mra_site'] ) ? (int) $_GET['mra_site'] : 0;
if ( $site_filter > 0 ) {
$where[] = 'site_id = %d';
$args[] = $site_filter;
}
// Status filter.
$valid_statuses = array( 'pending', 'queued', 'scanned', 'matches', 'error' );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$status_filter = isset( $_GET['mra_status'] ) && is_string( $_GET['mra_status'] ) ? sanitize_key( $_GET['mra_status'] ) : '';
if ( $status_filter && in_array( $status_filter, $valid_statuses, true ) ) {
$where[] = 'external_status = %s';
$args[] = $status_filter;
}
$where_sql = implode( ' AND ', $where );
// Sorting.
$valid_orderby = array( 'site_url', 'file_name', 'external_status', 'external_scanned_at' );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$orderby_raw = isset( $_GET['orderby'] ) && is_string( $_GET['orderby'] ) ? sanitize_key( $_GET['orderby'] ) : 'site_url';
$orderby = in_array( $orderby_raw, $valid_orderby, true ) ? $orderby_raw : 'site_url';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order_raw = isset( $_GET['order'] ) && is_string( $_GET['order'] ) ? strtoupper( sanitize_key( $_GET['order'] ) ) : 'ASC';
$order = in_array( $order_raw, array( 'ASC', 'DESC' ), true ) ? $order_raw : 'ASC';
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
$count_sql = "SELECT COUNT(*) FROM `{$table}` WHERE {$where_sql}";
$total = empty( $args ) ? (int) $wpdb->get_var( $count_sql ) : (int) $wpdb->get_var( $wpdb->prepare( $count_sql, ...$args ) );
$data_sql = "SELECT * FROM `{$table}` WHERE {$where_sql} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d";
$query_args = array_merge( $args, array( $per_pg, $offset ) );
$this->items = (array) $wpdb->get_results( $wpdb->prepare( $data_sql, ...$query_args ), ARRAY_A );
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
$this->set_pagination_args(
array(
'total_items' => $total,
'per_page' => $per_pg,
)
);
$this->_column_headers = array(
$this->get_columns(),
array(),
$this->get_sortable_columns(),
);
}
/**
* Renders the "no items" message.
*
* @return void
*/
public function no_items(): void {
esc_html_e( 'No media attachments found across the network.', 'robotstxt-mediaaudit' );
}
}