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 '
'; echo '

' . esc_html__( '← Media Audit', 'robotstxt-mediaaudit' ) . ' — ' . $filename . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $filename is already escaped via esc_html() above. // ----------------------------------------------------------------------- // File Information // ----------------------------------------------------------------------- echo '

' . esc_html__( 'File Information', 'robotstxt-mediaaudit' ) . '

'; echo ''; $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( '', esc_html__( 'ID', 'robotstxt-mediaaudit' ), esc_html( (string) $attachment_id ) ); printf( '', esc_html__( 'Filename', 'robotstxt-mediaaudit' ), esc_html( $fn_raw ) ); if ( '' !== $url_raw ) { printf( '', esc_html__( 'File URL', 'robotstxt-mediaaudit' ), esc_url( $url_raw ), esc_html( $url_raw ) ); } printf( '', 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( '', esc_html__( 'File size', 'robotstxt-mediaaudit' ), esc_html( $file_size_str ) ); printf( '', esc_html__( 'Internal scan date', 'robotstxt-mediaaudit' ), esc_html( $is_raw ) ); printf( '', esc_html__( 'External Status', 'robotstxt-mediaaudit' ), esc_html( $es_raw ) ); echo '
%s%s
%s%s
%s%s
%s%s
%s%s
%s%s
%s%s
'; // Thumbnail. $thumb = wp_get_attachment_image( $attachment_id, 'medium', false, array( 'class' => 'mra-detail-thumb' ) ); if ( $thumb ) { echo '

' . $thumb . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_get_attachment_image is safe. } // ----------------------------------------------------------------------- // Internal Usage // ----------------------------------------------------------------------- echo '

' . esc_html__( 'Internal Usage', 'robotstxt-mediaaudit' ) . '

'; $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 '

' . esc_html__( 'Not used in any post.', 'robotstxt-mediaaudit' ) . '

'; } else { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; 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 .= ' ' . esc_html( $meta_key ) . ''; } $edit_link = get_edit_post_link( $post_id ); $post_cell = $edit_link ? sprintf( '%s', esc_url( $edit_link ), esc_html( $post_title ) ) : esc_html( $post_title ); echo ''; echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $post_cell already escaped above. echo ''; echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $ctx_label already escaped above. echo ''; echo ''; } echo '
' . esc_html__( 'Post', 'robotstxt-mediaaudit' ) . '' . esc_html__( 'Type', 'robotstxt-mediaaudit' ) . '' . esc_html__( 'Context', 'robotstxt-mediaaudit' ) . '' . esc_html__( 'Status', 'robotstxt-mediaaudit' ) . '
' . $post_cell . '' . esc_html( $post_type ) . '' . $ctx_label . '' . esc_html( $status ) . '
'; } // ----------------------------------------------------------------------- // External Scan Results // ----------------------------------------------------------------------- echo '

' . esc_html__( 'External Scan Results', 'robotstxt-mediaaudit' ) . '

'; $this->render_external_results( $attachment_id ); echo '
'; // .wrap } // ------------------------------------------------------------------------- // Private helpers // ------------------------------------------------------------------------- /** * Fetches the mra_media_index row for a given attachment ID. * * @param int $attachment_id Attachment post ID. * * @return array|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 '

' . esc_html__( 'No external scan data available.', 'robotstxt-mediaaudit' ) . '

'; 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 '

'; 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 '

'; 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 $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 '

' . esc_html__( 'Alert Domains', 'robotstxt-mediaaudit' ) . '

'; echo '

' . esc_html__( 'These domains are associated with copyright-protected content and may indicate a copyright risk.', 'robotstxt-mediaaudit' ) . '

'; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ( $alert_domains as $domain => $count ) { echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__( 'Domain', 'robotstxt-mediaaudit' ) . '' . esc_html__( 'Occurrences', 'robotstxt-mediaaudit' ) . '
' . esc_html( $domain ) . '' . esc_html( (string) $count ) . '
'; } if ( ! empty( $other_domains ) ) { echo '

' . esc_html__( 'Other Domains', 'robotstxt-mediaaudit' ) . '

'; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ( $other_domains as $domain => $count ) { echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__( 'Domain', 'robotstxt-mediaaudit' ) . '' . esc_html__( 'Occurrences', 'robotstxt-mediaaudit' ) . '
' . esc_html( $domain ) . '' . esc_html( (string) $count ) . '
'; } if ( ! empty( $ignored_domains ) ) { echo '

' . esc_html__( 'Ignored Domains', 'robotstxt-mediaaudit' ) . '

'; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ( $ignored_domains as $domain => $count ) { echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__( 'Domain', 'robotstxt-mediaaudit' ) . '' . esc_html__( 'Occurrences', 'robotstxt-mediaaudit' ) . '
' . esc_html( $domain ) . '' . esc_html( (string) $count ) . '
'; } } /** * Renders the Google Cloud Vision result tables. * * @param array $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 ''; echo ''; echo ''; echo ''; echo ''; echo ''; 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 ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__( 'Pages with this image', 'robotstxt-mediaaudit' ) . '
' . esc_html__( 'Page URL', 'robotstxt-mediaaudit' ) . '' . esc_html__( 'Page Title', 'robotstxt-mediaaudit' ) . '
' . esc_html( $page_url ) . '' . esc_html( $page_title ) . '
'; } // Full matching images. $full_val = $web['fullMatchingImages'] ?? null; $full = is_array( $full_val ) ? $full_val : array(); if ( ! empty( $full ) ) { echo ''; echo ''; echo ''; 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 ''; } echo '
' . esc_html__( 'Full image matches', 'robotstxt-mediaaudit' ) . '
' . esc_html__( 'Image URL', 'robotstxt-mediaaudit' ) . '
' . esc_html( $img_url ) . '
'; } // Partial matching images. $partial_val = $web['partialMatchingImages'] ?? null; $partial = is_array( $partial_val ) ? $partial_val : array(); if ( ! empty( $partial ) ) { echo ''; echo ''; echo ''; 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 ''; } echo '
' . esc_html__( 'Partial image matches', 'robotstxt-mediaaudit' ) . '
' . esc_html__( 'Image URL', 'robotstxt-mediaaudit' ) . '
' . esc_html( $img_url ) . '
'; } } /** * Renders the TinEye backlinks table, sorted by crawl_date DESC. * * @param array $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 ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; 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 ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__( 'Backlinks found by TinEye (sorted by crawl date, newest first)', 'robotstxt-mediaaudit' ) . '
' . esc_html__( 'Page URL', 'robotstxt-mediaaudit' ) . '' . esc_html__( 'Image URL', 'robotstxt-mediaaudit' ) . '' . esc_html__( 'Crawl date', 'robotstxt-mediaaudit' ) . '
' . esc_html( $page_url ) . '' . esc_html( $img_url ) . '' . esc_html( $bl_date ) . '
'; } /** * Renders the PicDefense scan result sections. * * @param array $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 '

' . esc_html( ucfirst( $picrisk ) ) . '

'; } // 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 '

' . esc_html__( 'Risk flags', 'robotstxt-mediaaudit' ) . '

'; if ( empty( $flags ) ) { echo '

' . esc_html__( 'No risk flags detected.', 'robotstxt-mediaaudit' ) . '

'; } else { echo '
    '; foreach ( $flags as $flag ) { echo '
  • ' . $flag . '
  • '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped above. } echo '
'; } // 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 ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; 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 ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__( 'Backlinks found by PicDefense', 'robotstxt-mediaaudit' ) . '
' . esc_html__( 'Page URL', 'robotstxt-mediaaudit' ) . '' . esc_html__( 'Image URL', 'robotstxt-mediaaudit' ) . '' . esc_html__( 'Similarity', 'robotstxt-mediaaudit' ) . '
' . esc_html( $page_url ) . '' . esc_html( $img_url ) . '' . esc_html( $score ) . '
'; } // 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 '

' . esc_html__( 'Labels', 'robotstxt-mediaaudit' ) . '

'; echo '

' . esc_html( $label_str ) . '

'; } } } } }