304 lines
9.1 KiB
PHP
304 lines
9.1 KiB
PHP
<?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 '—';
|
|
}
|
|
|
|
/**
|
|
* 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 ) : '—';
|
|
}
|
|
|
|
/**
|
|
* 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' );
|
|
}
|
|
}
|