v1.6.0
This commit is contained in:
parent
83d629568a
commit
98b1cf0f3b
17 changed files with 2968 additions and 1313 deletions
|
|
@ -9,6 +9,7 @@ namespace MediaRightsAudit\Admin;
|
|||
|
||||
use MediaRightsAudit\Admin\AttachmentDetailPage;
|
||||
use MediaRightsAudit\External\ExternalScanner;
|
||||
use MediaRightsAudit\External\HostnameFilter;
|
||||
use MediaRightsAudit\External\ResultsConsolidator;
|
||||
use MediaRightsAudit\Internal\AttachmentIndexer;
|
||||
use MediaRightsAudit\Internal\UsageScanner;
|
||||
|
|
@ -65,9 +66,9 @@ class AuditPage {
|
|||
$action = $this->get_current_bulk_action();
|
||||
|
||||
if ( 'export_csv' === $action ) {
|
||||
$this->handle_export_csv();
|
||||
} elseif ( 'export_external_csv' === $action ) {
|
||||
$this->handle_export_external_csv();
|
||||
$this->handle_export_csv( false );
|
||||
} elseif ( 'export_csv_alerts' === $action ) {
|
||||
$this->handle_export_csv( true );
|
||||
} elseif ( 'run_external_scan' === $action ) {
|
||||
$this->handle_run_external_scan();
|
||||
} elseif ( 'purge_external_data' === $action ) {
|
||||
|
|
@ -346,22 +347,51 @@ class AuditPage {
|
|||
$html .= '</div>';
|
||||
|
||||
if ( ! empty( $domains ) ) {
|
||||
$html .= '<table class="widefat mra-domains-table">';
|
||||
$html .= '<thead><tr>';
|
||||
$html .= '<th>' . esc_html__( 'Domain', 'robotstxt-mediaaudit' ) . '</th>';
|
||||
$html .= '<th>' . esc_html__( 'Occurrences', 'robotstxt-mediaaudit' ) . '</th>';
|
||||
$html .= '</tr></thead><tbody>';
|
||||
$alert_domains = array();
|
||||
$other_domains = array();
|
||||
|
||||
foreach ( $domains as $domain => $count ) {
|
||||
if ( ! is_string( $domain ) ) {
|
||||
continue;
|
||||
}
|
||||
$html .= sprintf(
|
||||
'<tr><td>%s</td><td>%s</td></tr>',
|
||||
esc_html( $domain ),
|
||||
esc_html( (string) ( is_numeric( $count ) ? (int) $count : 0 ) )
|
||||
);
|
||||
$classification = HostnameFilter::classify( $domain );
|
||||
if ( 'alert' === $classification ) {
|
||||
$alert_domains[ $domain ] = is_numeric( $count ) ? (int) $count : 0;
|
||||
} elseif ( 'other' === $classification ) {
|
||||
$other_domains[ $domain ] = is_numeric( $count ) ? (int) $count : 0;
|
||||
}
|
||||
// Ignored domains are skipped entirely.
|
||||
}
|
||||
|
||||
$visible_domains = array_merge( $alert_domains, $other_domains );
|
||||
|
||||
if ( ! empty( $visible_domains ) ) {
|
||||
$html .= '<table class="widefat mra-domains-table">';
|
||||
$html .= '<thead><tr>';
|
||||
$html .= '<th>' . esc_html__( 'Domain', 'robotstxt-mediaaudit' ) . '</th>';
|
||||
$html .= '<th>' . esc_html__( 'Occurrences', 'robotstxt-mediaaudit' ) . '</th>';
|
||||
$html .= '<th>' . esc_html__( 'Status', 'robotstxt-mediaaudit' ) . '</th>';
|
||||
$html .= '</tr></thead><tbody>';
|
||||
|
||||
foreach ( $alert_domains as $domain => $count ) {
|
||||
$html .= sprintf(
|
||||
'<tr><td>%s</td><td>%s</td><td><span class="mra-badge mra-badge-alert">%s</span></td></tr>',
|
||||
esc_html( $domain ),
|
||||
esc_html( (string) $count ),
|
||||
esc_html__( 'Alert', 'robotstxt-mediaaudit' )
|
||||
);
|
||||
}
|
||||
|
||||
foreach ( $other_domains as $domain => $count ) {
|
||||
$html .= sprintf(
|
||||
'<tr><td>%s</td><td>%s</td><td></td></tr>',
|
||||
esc_html( $domain ),
|
||||
esc_html( (string) $count )
|
||||
);
|
||||
}
|
||||
|
||||
$html .= '</tbody></table>';
|
||||
}
|
||||
$html .= '</tbody></table>';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -452,6 +482,14 @@ class AuditPage {
|
|||
);
|
||||
}
|
||||
|
||||
if ( $stats['total_alert'] > 0 ) {
|
||||
$this->stat_box(
|
||||
$stats['total_alert'],
|
||||
__( 'Alert', 'robotstxt-mediaaudit' ),
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
|
|
@ -475,21 +513,26 @@ class AuditPage {
|
|||
}
|
||||
|
||||
/**
|
||||
* Outputs CSV for selected attachment IDs and terminates the request.
|
||||
* Outputs the unified CSV export and terminates the request.
|
||||
*
|
||||
* One row per attachment. When $alerts_only is true, only attachments that
|
||||
* have at least one alert domain match are included.
|
||||
*
|
||||
* @param bool $alerts_only When true, export only attachments with alert domains.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_export_csv(): void {
|
||||
private function handle_export_csv( bool $alerts_only ): void {
|
||||
check_admin_referer( 'bulk-mra-attachments' );
|
||||
|
||||
if ( ! current_user_can( 'edit_posts' ) ) {
|
||||
wp_die( esc_html__( 'Insufficient permissions.', 'robotstxt-mediaaudit' ) );
|
||||
}
|
||||
|
||||
$ids = $this->collect_attachment_ids();
|
||||
$rows = $this->build_csv_rows( $ids );
|
||||
|
||||
$filename = 'media-audit-' . gmdate( 'Y-m-d' ) . '.csv';
|
||||
$ids = $this->collect_attachment_ids();
|
||||
$rows = $this->build_unified_csv_rows( $ids, $alerts_only );
|
||||
$suffix = $alerts_only ? '-alerts' : '';
|
||||
$filename = 'media-audit' . $suffix . '-' . gmdate( 'Y-m-d' ) . '.csv';
|
||||
|
||||
header( 'Content-Type: text/csv; charset=utf-8' );
|
||||
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
|
||||
|
|
@ -511,9 +554,17 @@ class AuditPage {
|
|||
__( 'File URL', 'robotstxt-mediaaudit' ),
|
||||
__( 'MIME Type', 'robotstxt-mediaaudit' ),
|
||||
__( 'File Size (bytes)', 'robotstxt-mediaaudit' ),
|
||||
__( 'Internal Scan Date', 'robotstxt-mediaaudit' ),
|
||||
__( 'External Status', 'robotstxt-mediaaudit' ),
|
||||
__( 'Has Alert', 'robotstxt-mediaaudit' ),
|
||||
__( 'Usage Count', 'robotstxt-mediaaudit' ),
|
||||
__( 'Used In (post titles)', 'robotstxt-mediaaudit' ),
|
||||
__( 'Used In', 'robotstxt-mediaaudit' ),
|
||||
__( 'Google Vision — Matches', 'robotstxt-mediaaudit' ),
|
||||
__( 'TinEye — Matches', 'robotstxt-mediaaudit' ),
|
||||
__( 'PicDefense — Matches', 'robotstxt-mediaaudit' ),
|
||||
__( 'Alert Domains', 'robotstxt-mediaaudit' ),
|
||||
__( 'Other Domains', 'robotstxt-mediaaudit' ),
|
||||
__( 'Ignored Domains', 'robotstxt-mediaaudit' ),
|
||||
)
|
||||
);
|
||||
|
||||
|
|
@ -526,136 +577,188 @@ class AuditPage {
|
|||
}
|
||||
|
||||
/**
|
||||
* Outputs an external-results CSV for selected attachment IDs and terminates.
|
||||
* Builds unified CSV rows (one per attachment) with internal and external data.
|
||||
*
|
||||
* One row per attachment × provider pair. Attachments with no results still
|
||||
* appear with empty provider columns.
|
||||
* Columns: ID, filename, URL, MIME type, file size, internal scan date,
|
||||
* external status, has alert, usage count, used-in titles, per-provider
|
||||
* match counts, and classified domain lists (alert / other / ignored).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_export_external_csv(): void {
|
||||
check_admin_referer( 'bulk-mra-attachments' );
|
||||
|
||||
if ( ! current_user_can( 'edit_posts' ) ) {
|
||||
wp_die( esc_html__( 'Insufficient permissions.', 'robotstxt-mediaaudit' ) );
|
||||
}
|
||||
|
||||
$ids = $this->collect_attachment_ids();
|
||||
$rows = $this->build_external_csv_rows( $ids );
|
||||
|
||||
$filename = 'media-audit-external-' . gmdate( 'Y-m-d' ) . '.csv';
|
||||
|
||||
header( 'Content-Type: text/csv; charset=utf-8' );
|
||||
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
|
||||
header( 'Pragma: no-cache' );
|
||||
|
||||
$out = fopen( 'php://output', 'w' );
|
||||
if ( false === $out ) {
|
||||
wp_die( esc_html__( 'Could not open output stream.', 'robotstxt-mediaaudit' ) );
|
||||
}
|
||||
|
||||
fwrite( $out, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
|
||||
|
||||
fputcsv(
|
||||
$out,
|
||||
array(
|
||||
__( 'Attachment ID', 'robotstxt-mediaaudit' ),
|
||||
__( 'Filename', 'robotstxt-mediaaudit' ),
|
||||
__( 'File URL', 'robotstxt-mediaaudit' ),
|
||||
__( 'External Status', 'robotstxt-mediaaudit' ),
|
||||
__( 'Last Scanned', 'robotstxt-mediaaudit' ),
|
||||
__( 'Provider', 'robotstxt-mediaaudit' ),
|
||||
__( 'Match Count', 'robotstxt-mediaaudit' ),
|
||||
__( 'Top Domains', 'robotstxt-mediaaudit' ),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $rows as $row ) {
|
||||
fputcsv( $out, $row );
|
||||
}
|
||||
|
||||
fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds external-results CSV rows for the given attachment IDs.
|
||||
* When $ids is empty all indexed attachments are exported. When $alerts_only
|
||||
* is true, only attachments with at least one alert domain match are included.
|
||||
*
|
||||
* Each row represents one attachment × provider combination. Attachments with
|
||||
* no provider results are included with empty provider columns. If $ids is
|
||||
* empty, all indexed attachments are exported.
|
||||
*
|
||||
* @param array<int, int> $ids Attachment IDs to export (empty = all).
|
||||
* @param array<int, int> $ids Attachment IDs to export (empty = all).
|
||||
* @param bool $alerts_only Include only attachments with alert domains.
|
||||
*
|
||||
* @return array<int, array<int, string>>
|
||||
*/
|
||||
private function build_external_csv_rows( array $ids ): array {
|
||||
private function build_unified_csv_rows( array $ids, bool $alerts_only ): array {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
||||
|
||||
if ( empty( $ids ) ) {
|
||||
$rows = $wpdb->get_results(
|
||||
"SELECT i.attachment_id, i.file_name, i.file_url, i.external_status,
|
||||
i.external_scanned_at, er.provider, er.match_count, er.top_domains
|
||||
FROM {$wpdb->prefix}mra_media_index i
|
||||
LEFT JOIN {$wpdb->prefix}mra_external_results er ON er.attachment_id = i.attachment_id
|
||||
ORDER BY i.attachment_id ASC, er.provider ASC",
|
||||
$index_rows = $wpdb->get_results(
|
||||
"SELECT * FROM {$wpdb->prefix}mra_media_index ORDER BY attachment_id ASC",
|
||||
ARRAY_A
|
||||
);
|
||||
} else {
|
||||
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
|
||||
$rows = $wpdb->get_results(
|
||||
$index_rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT i.attachment_id, i.file_name, i.file_url, i.external_status,
|
||||
i.external_scanned_at, er.provider, er.match_count, er.top_domains
|
||||
FROM {$wpdb->prefix}mra_media_index i
|
||||
LEFT JOIN {$wpdb->prefix}mra_external_results er ON er.attachment_id = i.attachment_id
|
||||
WHERE i.attachment_id IN ({$placeholders})
|
||||
ORDER BY i.attachment_id ASC, er.provider ASC",
|
||||
"SELECT * FROM {$wpdb->prefix}mra_media_index WHERE attachment_id IN ({$placeholders}) ORDER BY attachment_id ASC",
|
||||
...$ids
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
||||
|
||||
if ( ! $rows ) {
|
||||
if ( ! is_array( $index_rows ) || empty( $index_rows ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$output = array();
|
||||
$page_ids = array_map(
|
||||
static function ( $v ): int {
|
||||
return (int) $v;
|
||||
},
|
||||
array_column( $index_rows, 'attachment_id' )
|
||||
);
|
||||
|
||||
foreach ( $rows as $r ) {
|
||||
$aid_raw = $r['attachment_id'] ?? '';
|
||||
$scanned_raw = $r['external_scanned_at'] ?? '';
|
||||
$domains_raw = $r['top_domains'] ?? '';
|
||||
$mc_raw = $r['match_count'] ?? '';
|
||||
// Fetch usages.
|
||||
$usages = MediaListTable::fetch_usages( $page_ids );
|
||||
$usage_titles = array();
|
||||
foreach ( $usages as $u ) {
|
||||
$aid_val = $u['attachment_id'] ?? null;
|
||||
$aid = is_numeric( $aid_val ) ? (int) $aid_val : 0;
|
||||
$t_raw = $u['post_title'] ?? '';
|
||||
$pid_raw = $u['post_id'] ?? null;
|
||||
$title = is_string( $t_raw ) && '' !== $t_raw
|
||||
? $t_raw
|
||||
: sprintf( '#%d', is_numeric( $pid_raw ) ? (int) $pid_raw : 0 );
|
||||
$usage_titles[ $aid ][] = $title;
|
||||
}
|
||||
|
||||
$domains_decoded = is_string( $domains_raw ) && '' !== $domains_raw
|
||||
? json_decode( $domains_raw, true )
|
||||
: array();
|
||||
$domain_parts = array();
|
||||
if ( is_array( $domains_decoded ) ) {
|
||||
foreach ( $domains_decoded as $d ) {
|
||||
if ( is_string( $d ) ) {
|
||||
$domain_parts[] = $d;
|
||||
// Fetch external results (all providers) for these attachments.
|
||||
$ext_placeholders = implode( ',', array_fill( 0, count( $page_ids ), '%d' ) );
|
||||
|
||||
$ext_rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT attachment_id, provider, match_count, top_domains
|
||||
FROM {$wpdb->prefix}mra_external_results
|
||||
WHERE attachment_id IN ({$ext_placeholders})",
|
||||
...$page_ids
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
||||
|
||||
/**
|
||||
* Per-attachment, per-provider data: match_count + merged domain→count.
|
||||
*
|
||||
* @var array<int, array<string, array{match_count: int, domains: array<string, int>}>> $ext_by_id
|
||||
*/
|
||||
$ext_by_id = array();
|
||||
|
||||
if ( is_array( $ext_rows ) ) {
|
||||
foreach ( $ext_rows as $er ) {
|
||||
if ( ! is_array( $er ) ) {
|
||||
continue;
|
||||
}
|
||||
$aid_val = $er['attachment_id'] ?? null;
|
||||
$eaid = is_numeric( $aid_val ) ? (int) $aid_val : 0;
|
||||
$prov_val = $er['provider'] ?? null;
|
||||
$provider = is_string( $prov_val ) ? $prov_val : '';
|
||||
$mc_val = $er['match_count'] ?? null;
|
||||
$mc = is_numeric( $mc_val ) ? (int) $mc_val : 0;
|
||||
$td_val = $er['top_domains'] ?? null;
|
||||
$td = is_string( $td_val ) && '' !== $td_val ? json_decode( $td_val, true ) : null;
|
||||
|
||||
if ( $eaid <= 0 || '' === $provider ) {
|
||||
continue;
|
||||
}
|
||||
if ( ! isset( $ext_by_id[ $eaid ][ $provider ] ) ) {
|
||||
$ext_by_id[ $eaid ][ $provider ] = array(
|
||||
'match_count' => 0,
|
||||
'domains' => array(),
|
||||
);
|
||||
}
|
||||
$ext_by_id[ $eaid ][ $provider ]['match_count'] += $mc;
|
||||
|
||||
if ( is_array( $td ) ) {
|
||||
foreach ( $td as $domain => $count ) {
|
||||
if ( ! is_string( $domain ) ) {
|
||||
continue;
|
||||
}
|
||||
$existing = $ext_by_id[ $eaid ][ $provider ]['domains'][ $domain ] ?? 0;
|
||||
$ext_by_id[ $eaid ][ $provider ]['domains'][ $domain ] = $existing + ( is_numeric( $count ) ? (int) $count : 0 );
|
||||
}
|
||||
}
|
||||
}
|
||||
$domains_str = implode( '; ', $domain_parts );
|
||||
}
|
||||
|
||||
$output[] = array(
|
||||
is_numeric( $aid_raw ) ? (string) (int) $aid_raw : '',
|
||||
$known_providers = array( 'google_vision', 'tineye', 'picdefense' );
|
||||
$output = array();
|
||||
|
||||
foreach ( $index_rows as $r ) {
|
||||
if ( ! is_array( $r ) ) {
|
||||
continue;
|
||||
}
|
||||
$aid_val = $r['attachment_id'] ?? null;
|
||||
$aid = is_numeric( $aid_val ) ? (int) $aid_val : 0;
|
||||
|
||||
// Aggregate domains across all providers for this attachment.
|
||||
$all_domains = array();
|
||||
foreach ( $known_providers as $p ) {
|
||||
$p_domains = $ext_by_id[ $aid ][ $p ]['domains'] ?? array();
|
||||
foreach ( $p_domains as $domain => $count ) {
|
||||
$existing = $all_domains[ $domain ] ?? 0;
|
||||
$all_domains[ $domain ] = $existing + $count;
|
||||
}
|
||||
}
|
||||
|
||||
// Classify domains.
|
||||
$alert_list = array();
|
||||
$other_list = array();
|
||||
$ignored_list = array();
|
||||
foreach ( array_keys( $all_domains ) as $domain ) {
|
||||
$class = HostnameFilter::classify( $domain );
|
||||
if ( 'alert' === $class ) {
|
||||
$alert_list[] = $domain;
|
||||
} elseif ( 'ignored' === $class ) {
|
||||
$ignored_list[] = $domain;
|
||||
} else {
|
||||
$other_list[] = $domain;
|
||||
}
|
||||
}
|
||||
|
||||
$has_alert = ! empty( $alert_list );
|
||||
|
||||
if ( $alerts_only && ! $has_alert ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$row_data = array(
|
||||
(string) $aid,
|
||||
is_string( $r['file_name'] ?? null ) ? (string) $r['file_name'] : '',
|
||||
is_string( $r['file_url'] ?? null ) ? (string) $r['file_url'] : '',
|
||||
is_string( $r['mime_type'] ?? null ) ? (string) $r['mime_type'] : '',
|
||||
is_numeric( $r['file_size'] ?? null ) ? (string) (int) $r['file_size'] : '',
|
||||
is_string( $r['internal_scanned_at'] ?? null ) ? (string) $r['internal_scanned_at'] : '',
|
||||
is_string( $r['external_status'] ?? null ) ? (string) $r['external_status'] : '',
|
||||
is_string( $scanned_raw ) ? $scanned_raw : '',
|
||||
is_string( $r['provider'] ?? null ) ? (string) $r['provider'] : '',
|
||||
is_numeric( $mc_raw ) ? (string) (int) $mc_raw : '',
|
||||
$domains_str,
|
||||
$has_alert ? __( 'Yes', 'robotstxt-mediaaudit' ) : __( 'No', 'robotstxt-mediaaudit' ),
|
||||
(string) count( $usage_titles[ $aid ] ?? array() ),
|
||||
implode( '; ', $usage_titles[ $aid ] ?? array() ),
|
||||
);
|
||||
|
||||
foreach ( $known_providers as $p ) {
|
||||
$row_data[] = isset( $ext_by_id[ $aid ][ $p ] ) ? (string) $ext_by_id[ $aid ][ $p ]['match_count'] : '';
|
||||
}
|
||||
|
||||
$row_data[] = implode( '; ', $alert_list );
|
||||
$row_data[] = implode( '; ', $other_list );
|
||||
$row_data[] = implode( '; ', $ignored_list );
|
||||
|
||||
$output[] = $row_data;
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
|
@ -745,87 +848,6 @@ class AuditPage {
|
|||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds CSV data rows for the given attachment IDs.
|
||||
*
|
||||
* If $ids is empty, exports all indexed attachments.
|
||||
*
|
||||
* @param array<int, int> $ids Attachment IDs to export (empty = all).
|
||||
*
|
||||
* @return array<int, array<int, string>>
|
||||
*/
|
||||
private function build_csv_rows( array $ids ): array {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
||||
|
||||
if ( empty( $ids ) ) {
|
||||
$index_rows = $wpdb->get_results(
|
||||
"SELECT * FROM {$wpdb->prefix}mra_media_index ORDER BY attachment_id ASC",
|
||||
ARRAY_A
|
||||
);
|
||||
} else {
|
||||
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
|
||||
$index_rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}mra_media_index WHERE attachment_id IN ({$placeholders}) ORDER BY attachment_id ASC",
|
||||
...$ids
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
||||
|
||||
if ( ! $index_rows ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$page_ids = array_map(
|
||||
static function ( $v ) {
|
||||
return (int) $v;
|
||||
},
|
||||
array_column( $index_rows, 'attachment_id' )
|
||||
);
|
||||
$usages = MediaListTable::fetch_usages( $page_ids );
|
||||
|
||||
$usage_titles = array();
|
||||
foreach ( $usages as $u ) {
|
||||
$aid_val = $u['attachment_id'] ?? null;
|
||||
$aid = is_numeric( $aid_val ) ? (int) $aid_val : 0;
|
||||
$t_raw = $u['post_title'] ?? '';
|
||||
$pid_raw = $u['post_id'] ?? null;
|
||||
$title = is_string( $t_raw ) && '' !== $t_raw ? $t_raw : sprintf( '#%d', is_numeric( $pid_raw ) ? (int) $pid_raw : 0 );
|
||||
|
||||
$usage_titles[ $aid ][] = $title;
|
||||
}
|
||||
|
||||
$output = array();
|
||||
foreach ( $index_rows as $r ) {
|
||||
$aid = (int) ( $r['attachment_id'] ?? 0 );
|
||||
$titles = isset( $usage_titles[ $aid ] ) ? implode( '; ', $usage_titles[ $aid ] ) : '';
|
||||
|
||||
$fn_raw = $r['file_name'] ?? '';
|
||||
$url_raw = $r['file_url'] ?? '';
|
||||
$mt_raw = $r['mime_type'] ?? '';
|
||||
$fs_raw = $r['file_size'] ?? '';
|
||||
$es_raw = $r['external_status'] ?? '';
|
||||
|
||||
$output[] = array(
|
||||
(string) $aid,
|
||||
is_string( $fn_raw ) ? $fn_raw : '',
|
||||
is_string( $url_raw ) ? $url_raw : '',
|
||||
is_string( $mt_raw ) ? $mt_raw : '',
|
||||
is_string( $fs_raw ) ? $fs_raw : '',
|
||||
is_string( $es_raw ) ? $es_raw : '',
|
||||
(string) count( $usage_titles[ $aid ] ?? array() ),
|
||||
$titles,
|
||||
);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the active bulk action from the form submission.
|
||||
*
|
||||
|
|
@ -844,7 +866,7 @@ class AuditPage {
|
|||
/**
|
||||
* Queries and returns dashboard statistics.
|
||||
*
|
||||
* @return array{total_indexed: int, total_scanned: int, total_used: int, total_unused: int, total_matches: int, pending_index: int}
|
||||
* @return array{total_indexed: int, total_scanned: int, total_used: int, total_unused: int, total_matches: int, total_alert: int, pending_index: int}
|
||||
*/
|
||||
private static function get_dashboard_stats(): array {
|
||||
global $wpdb;
|
||||
|
|
@ -856,13 +878,77 @@ class AuditPage {
|
|||
$total_matches = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}mra_media_index WHERE external_status = 'matches'" );
|
||||
// phpcs:enable WordPress.DB.DirectDatabaseQuery
|
||||
|
||||
$total_alert = self::get_alert_attachment_count();
|
||||
|
||||
return array(
|
||||
'total_indexed' => $total_indexed,
|
||||
'total_scanned' => $total_scanned,
|
||||
'total_used' => $total_used,
|
||||
'total_unused' => max( 0, $total_indexed - $total_used ),
|
||||
'total_matches' => $total_matches,
|
||||
'total_alert' => $total_alert,
|
||||
'pending_index' => AttachmentIndexer::get_pending_count(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of unique attachments that have at least one alert domain
|
||||
* in their external scan results.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function get_alert_attachment_count(): int {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$rows = $wpdb->get_results(
|
||||
"SELECT attachment_id, top_domains FROM {$wpdb->prefix}mra_external_results",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ( ! is_array( $rows ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Group top_domains by attachment_id and check for alert domains.
|
||||
$merged = array();
|
||||
|
||||
foreach ( $rows as $row ) {
|
||||
if ( ! is_array( $row ) ) {
|
||||
continue;
|
||||
}
|
||||
$aid_val = $row['attachment_id'] ?? null;
|
||||
$aid = is_numeric( $aid_val ) ? (int) $aid_val : 0;
|
||||
if ( $aid <= 0 ) {
|
||||
continue;
|
||||
}
|
||||
$td_val = $row['top_domains'] ?? null;
|
||||
$td_raw = is_string( $td_val ) ? json_decode( $td_val, true ) : null;
|
||||
$domains = is_array( $td_raw ) ? $td_raw : array();
|
||||
|
||||
if ( ! isset( $merged[ $aid ] ) ) {
|
||||
$merged[ $aid ] = array();
|
||||
}
|
||||
foreach ( $domains as $domain => $count ) {
|
||||
if ( ! is_string( $domain ) ) {
|
||||
continue;
|
||||
}
|
||||
$c = is_numeric( $count ) ? (int) $count : 0;
|
||||
if ( isset( $merged[ $aid ][ $domain ] ) ) {
|
||||
$merged[ $aid ][ $domain ] += $c;
|
||||
} else {
|
||||
$merged[ $aid ][ $domain ] = $c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$alert_count = 0;
|
||||
foreach ( $merged as $domains ) {
|
||||
if ( HostnameFilter::has_alert_domains( $domains ) ) {
|
||||
++$alert_count;
|
||||
}
|
||||
}
|
||||
|
||||
return $alert_count;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue