700 lines
24 KiB
PHP
700 lines
24 KiB
PHP
<?php
|
|
/**
|
|
* Admin page controller for the Attachment Detail (full scan report) view.
|
|
*
|
|
* @package MediaRightsAudit\Admin
|
|
*/
|
|
|
|
namespace MediaRightsAudit\Admin;
|
|
|
|
use MediaRightsAudit\External\HostnameFilter;
|
|
|
|
/**
|
|
* Registers and renders the hidden attachment detail admin page.
|
|
*
|
|
* Displays the full scan report for a single attachment, including:
|
|
* - File metadata from mra_media_index.
|
|
* - Internal usage (post references).
|
|
* - Raw external scan results per provider.
|
|
*/
|
|
class AttachmentDetailPage {
|
|
|
|
/**
|
|
* Returns the URL for the attachment detail page.
|
|
*
|
|
* @param int $attachment_id WordPress attachment post ID.
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function url( int $attachment_id ): string {
|
|
return admin_url( 'admin.php?page=robotstxt-mediaaudit-detail&attachment_id=' . $attachment_id );
|
|
}
|
|
|
|
/**
|
|
* Renders the full attachment detail page.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function render(): void {
|
|
if ( ! current_user_can( 'edit_posts' ) ) {
|
|
wp_die( esc_html__( 'You do not have permission to access this page.', 'robotstxt-mediaaudit' ) );
|
|
}
|
|
|
|
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only display page.
|
|
$raw_id = isset( $_GET['attachment_id'] ) && is_string( $_GET['attachment_id'] )
|
|
? (int) sanitize_text_field( wp_unslash( $_GET['attachment_id'] ) )
|
|
: 0;
|
|
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
|
$attachment_id = $raw_id;
|
|
|
|
if ( $attachment_id <= 0 ) {
|
|
wp_die( esc_html__( 'Invalid attachment ID.', 'robotstxt-mediaaudit' ) );
|
|
}
|
|
|
|
$post = get_post( $attachment_id );
|
|
if ( null === $post || 'attachment' !== $post->post_type ) {
|
|
wp_die( esc_html__( 'Invalid attachment ID.', 'robotstxt-mediaaudit' ) );
|
|
}
|
|
|
|
$filename = esc_html( '' !== $post->post_title ? $post->post_title : sprintf( '#%d', $attachment_id ) );
|
|
$back_url = admin_url( 'admin.php?page=robotstxt-mediaaudit' );
|
|
$index_row = $this->get_index_row( $attachment_id );
|
|
|
|
echo '<div class="wrap">';
|
|
echo '<h1><a href="' . esc_url( $back_url ) . '">' . esc_html__( '← Media Audit', 'robotstxt-mediaaudit' ) . '</a> — ' . $filename . '</h1>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $filename is already escaped via esc_html() above.
|
|
|
|
// -----------------------------------------------------------------------
|
|
// File Information
|
|
// -----------------------------------------------------------------------
|
|
echo '<h2>' . esc_html__( 'File Information', 'robotstxt-mediaaudit' ) . '</h2>';
|
|
echo '<table class="widefat mra-detail-meta"><tbody>';
|
|
|
|
$fn_raw = isset( $index_row['file_name'] ) && is_string( $index_row['file_name'] ) ? $index_row['file_name'] : '';
|
|
$url_raw = isset( $index_row['file_url'] ) && is_string( $index_row['file_url'] ) ? $index_row['file_url'] : '';
|
|
$mt_raw = isset( $index_row['mime_type'] ) && is_string( $index_row['mime_type'] ) ? $index_row['mime_type'] : '';
|
|
$fs_raw = isset( $index_row['file_size'] ) ? $index_row['file_size'] : null;
|
|
$is_raw = isset( $index_row['internal_scanned_at'] ) && is_string( $index_row['internal_scanned_at'] ) ? $index_row['internal_scanned_at'] : '';
|
|
$es_raw = isset( $index_row['external_status'] ) && is_string( $index_row['external_status'] ) ? $index_row['external_status'] : '';
|
|
|
|
printf(
|
|
'<tr><th>%s</th><td>%s</td></tr>',
|
|
esc_html__( 'ID', 'robotstxt-mediaaudit' ),
|
|
esc_html( (string) $attachment_id )
|
|
);
|
|
|
|
printf(
|
|
'<tr><th>%s</th><td>%s</td></tr>',
|
|
esc_html__( 'Filename', 'robotstxt-mediaaudit' ),
|
|
esc_html( $fn_raw )
|
|
);
|
|
|
|
if ( '' !== $url_raw ) {
|
|
printf(
|
|
'<tr><th>%s</th><td><a href="%s" target="_blank">%s</a></td></tr>',
|
|
esc_html__( 'File URL', 'robotstxt-mediaaudit' ),
|
|
esc_url( $url_raw ),
|
|
esc_html( $url_raw )
|
|
);
|
|
}
|
|
|
|
printf(
|
|
'<tr><th>%s</th><td>%s</td></tr>',
|
|
esc_html__( 'MIME Type', 'robotstxt-mediaaudit' ),
|
|
esc_html( $mt_raw )
|
|
);
|
|
|
|
$file_size_str = '';
|
|
if ( null !== $fs_raw && is_numeric( $fs_raw ) && (int) $fs_raw > 0 ) {
|
|
$file_size_str = size_format( (int) $fs_raw );
|
|
}
|
|
printf(
|
|
'<tr><th>%s</th><td>%s</td></tr>',
|
|
esc_html__( 'File size', 'robotstxt-mediaaudit' ),
|
|
esc_html( $file_size_str )
|
|
);
|
|
|
|
printf(
|
|
'<tr><th>%s</th><td>%s</td></tr>',
|
|
esc_html__( 'Internal scan date', 'robotstxt-mediaaudit' ),
|
|
esc_html( $is_raw )
|
|
);
|
|
|
|
printf(
|
|
'<tr><th>%s</th><td>%s</td></tr>',
|
|
esc_html__( 'External Status', 'robotstxt-mediaaudit' ),
|
|
esc_html( $es_raw )
|
|
);
|
|
|
|
echo '</tbody></table>';
|
|
|
|
// Thumbnail.
|
|
$thumb = wp_get_attachment_image( $attachment_id, 'medium', false, array( 'class' => 'mra-detail-thumb' ) );
|
|
if ( $thumb ) {
|
|
echo '<p>' . $thumb . '</p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_get_attachment_image is safe.
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Internal Usage
|
|
// -----------------------------------------------------------------------
|
|
echo '<h2>' . esc_html__( 'Internal Usage', 'robotstxt-mediaaudit' ) . '</h2>';
|
|
|
|
$usages = MediaListTable::fetch_usages( array( $attachment_id ) );
|
|
|
|
$context_labels = array(
|
|
'featured' => __( 'Featured Image', 'robotstxt-mediaaudit' ),
|
|
'content' => __( 'Post Content', 'robotstxt-mediaaudit' ),
|
|
'meta' => __( 'Custom Field', 'robotstxt-mediaaudit' ),
|
|
);
|
|
|
|
if ( empty( $usages ) ) {
|
|
echo '<p>' . esc_html__( 'Not used in any post.', 'robotstxt-mediaaudit' ) . '</p>';
|
|
} else {
|
|
echo '<table class="widefat mra-usages-table">';
|
|
echo '<thead><tr>';
|
|
echo '<th>' . esc_html__( 'Post', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '<th>' . esc_html__( 'Type', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '<th>' . esc_html__( 'Context', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '<th>' . esc_html__( 'Status', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '</tr></thead><tbody>';
|
|
|
|
foreach ( $usages as $u ) {
|
|
$pid_val = $u['post_id'] ?? null;
|
|
$post_id = is_numeric( $pid_val ) ? (int) $pid_val : 0;
|
|
$t_raw = $u['post_title'] ?? '';
|
|
$post_title = is_string( $t_raw ) && '' !== $t_raw ? $t_raw : sprintf( '#%d', $post_id );
|
|
$pt_val = $u['post_type'] ?? null;
|
|
$post_type = is_string( $pt_val ) ? $pt_val : '';
|
|
$ctx_val = $u['context'] ?? null;
|
|
$context = is_string( $ctx_val ) ? $ctx_val : '';
|
|
$status_val = $u['post_status'] ?? null;
|
|
$status = is_string( $status_val ) ? $status_val : '';
|
|
$mk_val = $u['meta_key'] ?? null;
|
|
$meta_key = is_string( $mk_val ) ? $mk_val : '';
|
|
|
|
$ctx_label = isset( $context_labels[ $context ] ) ? $context_labels[ $context ] : esc_html( $context );
|
|
if ( '' !== $meta_key ) {
|
|
$ctx_label .= ' <code>' . esc_html( $meta_key ) . '</code>';
|
|
}
|
|
|
|
$edit_link = get_edit_post_link( $post_id );
|
|
$post_cell = $edit_link
|
|
? sprintf( '<a href="%s">%s</a>', esc_url( $edit_link ), esc_html( $post_title ) )
|
|
: esc_html( $post_title );
|
|
|
|
echo '<tr>';
|
|
echo '<td>' . $post_cell . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $post_cell already escaped above.
|
|
echo '<td>' . esc_html( $post_type ) . '</td>';
|
|
echo '<td>' . $ctx_label . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $ctx_label already escaped above.
|
|
echo '<td>' . esc_html( $status ) . '</td>';
|
|
echo '</tr>';
|
|
}
|
|
|
|
echo '</tbody></table>';
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// External Scan Results
|
|
// -----------------------------------------------------------------------
|
|
echo '<h2>' . esc_html__( 'External Scan Results', 'robotstxt-mediaaudit' ) . '</h2>';
|
|
$this->render_external_results( $attachment_id );
|
|
|
|
echo '</div>'; // .wrap
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Private helpers
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Fetches the mra_media_index row for a given attachment ID.
|
|
*
|
|
* @param int $attachment_id Attachment post ID.
|
|
*
|
|
* @return array<array-key, mixed>|null
|
|
*/
|
|
private function get_index_row( int $attachment_id ): ?array {
|
|
global $wpdb;
|
|
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$row = $wpdb->get_row(
|
|
$wpdb->prepare(
|
|
"SELECT file_url, file_name, mime_type, file_size, internal_scanned_at, external_status, external_scanned_at
|
|
FROM {$wpdb->prefix}mra_media_index
|
|
WHERE attachment_id = %d
|
|
LIMIT 1",
|
|
$attachment_id
|
|
),
|
|
ARRAY_A
|
|
);
|
|
|
|
return is_array( $row ) ? $row : null;
|
|
}
|
|
|
|
/**
|
|
* Fetches and renders the external scan results for a given attachment.
|
|
*
|
|
* After rendering all per-provider sections, aggregates top_domains across
|
|
* all providers and passes them to render_domain_classification() for a
|
|
* consolidated view grouped by alert / other / ignored status.
|
|
*
|
|
* @param int $attachment_id Attachment post ID.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function render_external_results( int $attachment_id ): void {
|
|
global $wpdb;
|
|
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$rows = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT provider, match_count, top_domains, raw_response, created_at
|
|
FROM {$wpdb->prefix}mra_external_results
|
|
WHERE attachment_id = %d
|
|
ORDER BY provider ASC",
|
|
$attachment_id
|
|
),
|
|
ARRAY_A
|
|
);
|
|
|
|
if ( empty( $rows ) ) {
|
|
echo '<p>' . esc_html__( 'No external scan data available.', 'robotstxt-mediaaudit' ) . '</p>';
|
|
return;
|
|
}
|
|
|
|
$provider_names = array(
|
|
'google_vision' => 'Google Cloud Vision',
|
|
'tineye' => 'TinEye',
|
|
'picdefense' => 'PicDefense',
|
|
);
|
|
|
|
$merged_domains = array();
|
|
|
|
foreach ( $rows as $row ) {
|
|
if ( ! is_array( $row ) ) {
|
|
continue;
|
|
}
|
|
|
|
$slug_val = $row['provider'] ?? null;
|
|
$slug = is_string( $slug_val ) ? $slug_val : '';
|
|
$name = isset( $provider_names[ $slug ] ) ? $provider_names[ $slug ] : ucwords( str_replace( '_', ' ', $slug ) );
|
|
$mc_val = $row['match_count'] ?? null;
|
|
$match_count = is_numeric( $mc_val ) ? (int) $mc_val : 0;
|
|
$at_val = $row['created_at'] ?? null;
|
|
$scanned_at = is_string( $at_val ) ? $at_val : '';
|
|
$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();
|
|
$rr_val = $row['raw_response'] ?? null;
|
|
$raw = null;
|
|
if ( is_string( $rr_val ) && '' !== $rr_val ) {
|
|
$decoded = json_decode( $rr_val, true );
|
|
$raw = is_array( $decoded ) ? $decoded : null;
|
|
}
|
|
|
|
// Accumulate top_domains across all providers.
|
|
foreach ( $domains as $domain => $count ) {
|
|
if ( ! is_string( $domain ) ) {
|
|
continue;
|
|
}
|
|
$c = is_numeric( $count ) ? (int) $count : 0;
|
|
if ( isset( $merged_domains[ $domain ] ) ) {
|
|
$merged_domains[ $domain ] += $c;
|
|
} else {
|
|
$merged_domains[ $domain ] = $c;
|
|
}
|
|
}
|
|
|
|
echo '<h3>';
|
|
echo esc_html( $name );
|
|
echo ' — ';
|
|
echo esc_html(
|
|
sprintf(
|
|
/* translators: %d: number of external matches */
|
|
_n( '%d match', '%d matches', $match_count, 'robotstxt-mediaaudit' ),
|
|
$match_count
|
|
)
|
|
);
|
|
if ( '' !== $scanned_at ) {
|
|
echo ' — ' . esc_html(
|
|
sprintf(
|
|
/* translators: %s: scan date string */
|
|
__( 'scanned %s', 'robotstxt-mediaaudit' ),
|
|
$scanned_at
|
|
)
|
|
);
|
|
}
|
|
echo '</h3>';
|
|
|
|
if ( null !== $raw ) {
|
|
if ( 'google_vision' === $slug ) {
|
|
$this->render_google_vision_tables( $raw );
|
|
} elseif ( 'tineye' === $slug ) {
|
|
$this->render_tineye_table( $raw );
|
|
} elseif ( 'picdefense' === $slug ) {
|
|
$this->render_picdefense_table( $raw );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Render consolidated domain classification section.
|
|
if ( ! empty( $merged_domains ) ) {
|
|
$this->render_domain_classification( $merged_domains );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders a domain classification summary grouped into Alert, Other, and Ignored sections.
|
|
*
|
|
* Each section is rendered only when non-empty. Alert domains are shown with
|
|
* a warning note about copyright risk.
|
|
*
|
|
* @param array<string, int> $domains Map of domain => total occurrence count.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function render_domain_classification( array $domains ): void {
|
|
$alert_domains = array();
|
|
$other_domains = array();
|
|
$ignored_domains = array();
|
|
|
|
foreach ( $domains as $domain => $count ) {
|
|
$classification = HostnameFilter::classify( $domain );
|
|
if ( 'alert' === $classification ) {
|
|
$alert_domains[ $domain ] = $count;
|
|
} elseif ( 'ignored' === $classification ) {
|
|
$ignored_domains[ $domain ] = $count;
|
|
} else {
|
|
$other_domains[ $domain ] = $count;
|
|
}
|
|
}
|
|
|
|
if ( ! empty( $alert_domains ) ) {
|
|
echo '<h3>' . esc_html__( 'Alert Domains', 'robotstxt-mediaaudit' ) . '</h3>';
|
|
echo '<p class="description">' . esc_html__( 'These domains are associated with copyright-protected content and may indicate a copyright risk.', 'robotstxt-mediaaudit' ) . '</p>';
|
|
echo '<table class="widefat mra-detail-table">';
|
|
echo '<thead><tr>';
|
|
echo '<th>' . esc_html__( 'Domain', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '<th>' . esc_html__( 'Occurrences', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '</tr></thead><tbody>';
|
|
foreach ( $alert_domains as $domain => $count ) {
|
|
echo '<tr>';
|
|
echo '<td>' . esc_html( $domain ) . '</td>';
|
|
echo '<td>' . esc_html( (string) $count ) . '</td>';
|
|
echo '</tr>';
|
|
}
|
|
echo '</tbody></table>';
|
|
}
|
|
|
|
if ( ! empty( $other_domains ) ) {
|
|
echo '<h3>' . esc_html__( 'Other Domains', 'robotstxt-mediaaudit' ) . '</h3>';
|
|
echo '<table class="widefat mra-detail-table">';
|
|
echo '<thead><tr>';
|
|
echo '<th>' . esc_html__( 'Domain', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '<th>' . esc_html__( 'Occurrences', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '</tr></thead><tbody>';
|
|
foreach ( $other_domains as $domain => $count ) {
|
|
echo '<tr>';
|
|
echo '<td>' . esc_html( $domain ) . '</td>';
|
|
echo '<td>' . esc_html( (string) $count ) . '</td>';
|
|
echo '</tr>';
|
|
}
|
|
echo '</tbody></table>';
|
|
}
|
|
|
|
if ( ! empty( $ignored_domains ) ) {
|
|
echo '<h3>' . esc_html__( 'Ignored Domains', 'robotstxt-mediaaudit' ) . '</h3>';
|
|
echo '<table class="widefat mra-detail-table">';
|
|
echo '<thead><tr>';
|
|
echo '<th>' . esc_html__( 'Domain', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '<th>' . esc_html__( 'Occurrences', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '</tr></thead><tbody>';
|
|
foreach ( $ignored_domains as $domain => $count ) {
|
|
echo '<tr>';
|
|
echo '<td>' . esc_html( $domain ) . '</td>';
|
|
echo '<td>' . esc_html( (string) $count ) . '</td>';
|
|
echo '</tr>';
|
|
}
|
|
echo '</tbody></table>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders the Google Cloud Vision result tables.
|
|
*
|
|
* @param array<mixed> $raw Decoded raw_response array.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function render_google_vision_tables( array $raw ): void {
|
|
$responses_val = $raw['responses'] ?? null;
|
|
if ( ! is_array( $responses_val ) || empty( $responses_val ) ) {
|
|
return;
|
|
}
|
|
|
|
$first = $responses_val[0] ?? null;
|
|
if ( ! is_array( $first ) ) {
|
|
return;
|
|
}
|
|
|
|
$web_val = $first['webDetection'] ?? null;
|
|
$web = is_array( $web_val ) ? $web_val : array();
|
|
|
|
// Pages with matching images.
|
|
$pages_val = $web['pagesWithMatchingImages'] ?? null;
|
|
$pages = is_array( $pages_val ) ? $pages_val : array();
|
|
if ( ! empty( $pages ) ) {
|
|
echo '<table class="widefat mra-detail-table">';
|
|
echo '<caption>' . esc_html__( 'Pages with this image', 'robotstxt-mediaaudit' ) . '</caption>';
|
|
echo '<thead><tr>';
|
|
echo '<th>' . esc_html__( 'Page URL', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '<th>' . esc_html__( 'Page Title', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '</tr></thead><tbody>';
|
|
foreach ( $pages as $page ) {
|
|
if ( ! is_array( $page ) ) {
|
|
continue;
|
|
}
|
|
$page_url_val = $page['url'] ?? null;
|
|
$page_url = is_string( $page_url_val ) ? $page_url_val : '';
|
|
$page_title_val = $page['pageTitle'] ?? null;
|
|
$page_title = is_string( $page_title_val ) ? $page_title_val : '';
|
|
echo '<tr>';
|
|
echo '<td><a href="' . esc_url( $page_url ) . '" target="_blank">' . esc_html( $page_url ) . '</a></td>';
|
|
echo '<td>' . esc_html( $page_title ) . '</td>';
|
|
echo '</tr>';
|
|
}
|
|
echo '</tbody></table>';
|
|
}
|
|
|
|
// Full matching images.
|
|
$full_val = $web['fullMatchingImages'] ?? null;
|
|
$full = is_array( $full_val ) ? $full_val : array();
|
|
if ( ! empty( $full ) ) {
|
|
echo '<table class="widefat mra-detail-table">';
|
|
echo '<caption>' . esc_html__( 'Full image matches', 'robotstxt-mediaaudit' ) . '</caption>';
|
|
echo '<thead><tr><th>' . esc_html__( 'Image URL', 'robotstxt-mediaaudit' ) . '</th></tr></thead><tbody>';
|
|
foreach ( $full as $img ) {
|
|
if ( ! is_array( $img ) ) {
|
|
continue;
|
|
}
|
|
$img_url_val = $img['url'] ?? null;
|
|
$img_url = is_string( $img_url_val ) ? $img_url_val : '';
|
|
echo '<tr><td><a href="' . esc_url( $img_url ) . '" target="_blank">' . esc_html( $img_url ) . '</a></td></tr>';
|
|
}
|
|
echo '</tbody></table>';
|
|
}
|
|
|
|
// Partial matching images.
|
|
$partial_val = $web['partialMatchingImages'] ?? null;
|
|
$partial = is_array( $partial_val ) ? $partial_val : array();
|
|
if ( ! empty( $partial ) ) {
|
|
echo '<table class="widefat mra-detail-table">';
|
|
echo '<caption>' . esc_html__( 'Partial image matches', 'robotstxt-mediaaudit' ) . '</caption>';
|
|
echo '<thead><tr><th>' . esc_html__( 'Image URL', 'robotstxt-mediaaudit' ) . '</th></tr></thead><tbody>';
|
|
foreach ( $partial as $img ) {
|
|
if ( ! is_array( $img ) ) {
|
|
continue;
|
|
}
|
|
$img_url_val = $img['url'] ?? null;
|
|
$img_url = is_string( $img_url_val ) ? $img_url_val : '';
|
|
echo '<tr><td><a href="' . esc_url( $img_url ) . '" target="_blank">' . esc_html( $img_url ) . '</a></td></tr>';
|
|
}
|
|
echo '</tbody></table>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders the TinEye backlinks table, sorted by crawl_date DESC.
|
|
*
|
|
* @param array<mixed> $raw Decoded raw_response array.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function render_tineye_table( array $raw ): void {
|
|
$results_val = $raw['results'] ?? null;
|
|
$results = is_array( $results_val ) ? $results_val : array();
|
|
$matches_val = $results['matches'] ?? null;
|
|
$matches = is_array( $matches_val ) ? $matches_val : array();
|
|
|
|
// Flatten all backlinks from all matches.
|
|
$backlinks = array();
|
|
foreach ( $matches as $match ) {
|
|
if ( ! is_array( $match ) ) {
|
|
continue;
|
|
}
|
|
$bl_val = $match['backlinks'] ?? null;
|
|
$bls = is_array( $bl_val ) ? $bl_val : array();
|
|
foreach ( $bls as $bl ) {
|
|
if ( is_array( $bl ) ) {
|
|
$backlinks[] = $bl;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( empty( $backlinks ) ) {
|
|
return;
|
|
}
|
|
|
|
// Sort by crawl_date DESC.
|
|
usort(
|
|
$backlinks,
|
|
static function ( array $a, array $b ): int {
|
|
$da = isset( $a['crawl_date'] ) && is_string( $a['crawl_date'] ) ? $a['crawl_date'] : '';
|
|
$db = isset( $b['crawl_date'] ) && is_string( $b['crawl_date'] ) ? $b['crawl_date'] : '';
|
|
return strcmp( $db, $da );
|
|
}
|
|
);
|
|
|
|
echo '<table class="widefat mra-detail-table">';
|
|
echo '<caption>' . esc_html__( 'Backlinks found by TinEye (sorted by crawl date, newest first)', 'robotstxt-mediaaudit' ) . '</caption>';
|
|
echo '<thead><tr>';
|
|
echo '<th>' . esc_html__( 'Page URL', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '<th>' . esc_html__( 'Image URL', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '<th>' . esc_html__( 'Crawl date', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '</tr></thead><tbody>';
|
|
|
|
foreach ( $backlinks as $bl ) {
|
|
// 'backlink' = page URL; 'url' = direct image URL.
|
|
$page_url_val = $bl['backlink'] ?? null;
|
|
$page_url = is_string( $page_url_val ) ? $page_url_val : '';
|
|
$img_url_val = $bl['url'] ?? null;
|
|
$img_url = is_string( $img_url_val ) ? $img_url_val : '';
|
|
$bl_date_val = $bl['crawl_date'] ?? null;
|
|
$bl_date = is_string( $bl_date_val ) ? $bl_date_val : '';
|
|
echo '<tr>';
|
|
echo '<td><a href="' . esc_url( $page_url ) . '" target="_blank">' . esc_html( $page_url ) . '</a></td>';
|
|
echo '<td><a href="' . esc_url( $img_url ) . '" target="_blank">' . esc_html( $img_url ) . '</a></td>';
|
|
echo '<td>' . esc_html( $bl_date ) . '</td>';
|
|
echo '</tr>';
|
|
}
|
|
|
|
echo '</tbody></table>';
|
|
}
|
|
|
|
/**
|
|
* Renders the PicDefense scan result sections.
|
|
*
|
|
* @param array<mixed> $raw Decoded raw_response array.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function render_picdefense_table( array $raw ): void {
|
|
$data_raw = $raw['data'] ?? null;
|
|
$data_arr = is_array( $data_raw ) ? $data_raw : array();
|
|
$data = isset( $data_arr[0] ) && is_array( $data_arr[0] ) ? $data_arr[0] : array();
|
|
|
|
if ( empty( $data ) ) {
|
|
return;
|
|
}
|
|
|
|
// picRisk badge.
|
|
$picrisk_val = $data['picrisk'] ?? null;
|
|
$picrisk = is_string( $picrisk_val ) ? strtolower( $picrisk_val ) : '';
|
|
if ( '' !== $picrisk ) {
|
|
echo '<p><span class="mra-badge mra-picrisk-' . esc_attr( $picrisk ) . '">' . esc_html( ucfirst( $picrisk ) ) . '</span></p>';
|
|
}
|
|
|
|
// Risk flags.
|
|
$flags = array();
|
|
if ( true === ( $data['face'] ?? null ) ) {
|
|
$flags[] = esc_html__( 'Face detected', 'robotstxt-mediaaudit' );
|
|
}
|
|
if ( true === ( $data['logo'] ?? null ) ) {
|
|
$flags[] = esc_html__( 'Logo detected', 'robotstxt-mediaaudit' );
|
|
}
|
|
if ( true === ( $data['landmark'] ?? null ) ) {
|
|
$flags[] = esc_html__( 'Landmark detected', 'robotstxt-mediaaudit' );
|
|
}
|
|
if ( true === ( $data['stock'] ?? null ) ) {
|
|
$flags[] = esc_html__( 'Stock image', 'robotstxt-mediaaudit' );
|
|
}
|
|
if ( true === ( $data['exif_copyrighted'] ?? null ) ) {
|
|
$holder_val = $data['exif_copyrightHolder'] ?? '';
|
|
$holder = is_string( $holder_val ) ? $holder_val : '';
|
|
$flags[] = sprintf(
|
|
/* translators: %s: copyright holder name */
|
|
esc_html__( 'EXIF copyright: %s', 'robotstxt-mediaaudit' ),
|
|
esc_html( $holder )
|
|
);
|
|
}
|
|
|
|
echo '<h4>' . esc_html__( 'Risk flags', 'robotstxt-mediaaudit' ) . '</h4>';
|
|
if ( empty( $flags ) ) {
|
|
echo '<p>' . esc_html__( 'No risk flags detected.', 'robotstxt-mediaaudit' ) . '</p>';
|
|
} else {
|
|
echo '<ul>';
|
|
foreach ( $flags as $flag ) {
|
|
echo '<li>' . $flag . '</li>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped above.
|
|
}
|
|
echo '</ul>';
|
|
}
|
|
|
|
// Backlinks table.
|
|
$backlinks_raw = $data['backlinks'] ?? null;
|
|
$backlinks = is_array( $backlinks_raw ) ? $backlinks_raw : array();
|
|
|
|
// Filter to valid entries only.
|
|
$valid_backlinks = array();
|
|
foreach ( $backlinks as $bl ) {
|
|
if ( is_array( $bl ) ) {
|
|
$valid_backlinks[] = $bl;
|
|
}
|
|
}
|
|
|
|
if ( ! empty( $valid_backlinks ) ) {
|
|
// Sort by similarity_score DESC.
|
|
usort(
|
|
$valid_backlinks,
|
|
static function ( array $a, array $b ): int {
|
|
$sa = isset( $a['similarity_score'] ) && is_numeric( $a['similarity_score'] ) ? (float) $a['similarity_score'] : 0.0;
|
|
$sb = isset( $b['similarity_score'] ) && is_numeric( $b['similarity_score'] ) ? (float) $b['similarity_score'] : 0.0;
|
|
if ( $sb > $sa ) {
|
|
return 1;
|
|
}
|
|
if ( $sb < $sa ) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
);
|
|
|
|
echo '<table class="widefat mra-detail-table">';
|
|
echo '<caption>' . esc_html__( 'Backlinks found by PicDefense', 'robotstxt-mediaaudit' ) . '</caption>';
|
|
echo '<thead><tr>';
|
|
echo '<th>' . esc_html__( 'Page URL', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '<th>' . esc_html__( 'Image URL', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '<th>' . esc_html__( 'Similarity', 'robotstxt-mediaaudit' ) . '</th>';
|
|
echo '</tr></thead><tbody>';
|
|
|
|
foreach ( $valid_backlinks as $bl ) {
|
|
$page_url_val = $bl['url'] ?? null;
|
|
$page_url = is_string( $page_url_val ) ? $page_url_val : '';
|
|
$img_url_val = $bl['backlink_image_url'] ?? null;
|
|
$img_url = is_string( $img_url_val ) ? $img_url_val : '';
|
|
$score_val = $bl['similarity_score'] ?? null;
|
|
$score = is_numeric( $score_val ) ? number_format( (float) $score_val, 2 ) : '';
|
|
echo '<tr>';
|
|
echo '<td><a href="' . esc_url( $page_url ) . '" target="_blank">' . esc_html( $page_url ) . '</a></td>';
|
|
echo '<td><a href="' . esc_url( $img_url ) . '" target="_blank">' . esc_html( $img_url ) . '</a></td>';
|
|
echo '<td>' . esc_html( $score ) . '</td>';
|
|
echo '</tr>';
|
|
}
|
|
|
|
echo '</tbody></table>';
|
|
}
|
|
|
|
// Labels.
|
|
$labels_val = $data['labels'] ?? null;
|
|
if ( is_array( $labels_val ) ) {
|
|
$labels_with_score_val = $labels_val['labelsWithScore'] ?? null;
|
|
$labels_with_score = is_array( $labels_with_score_val ) ? $labels_with_score_val : array();
|
|
if ( ! empty( $labels_with_score ) ) {
|
|
$label_str_val = $labels_with_score[0] ?? null;
|
|
$label_str = is_string( $label_str_val ) ? $label_str_val : '';
|
|
if ( '' !== $label_str ) {
|
|
echo '<h4>' . esc_html__( 'Labels', 'robotstxt-mediaaudit' ) . '</h4>';
|
|
echo '<p>' . esc_html( $label_str ) . '</p>';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|