diff --git a/assets/css/media-audit-admin.css b/assets/css/media-audit-admin.css index 0c4fa33..8250db8 100644 --- a/assets/css/media-audit-admin.css +++ b/assets/css/media-audit-admin.css @@ -102,6 +102,21 @@ color: #856404; } +.mra-picrisk-high { + background: #f8d7da; + color: #721c24; +} + +.mra-picrisk-medium { + background: #fff3cd; + color: #856404; +} + +.mra-picrisk-low { + background: #d4edda; + color: #155724; +} + .mra-badge-match-count { background: #721c24; color: #fff; diff --git a/changelog.txt b/changelog.txt index 701c3f2..4905d32 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,57 @@ == Changelog == += 1.5.0 = + +_Release date: 2026-05-05_ + +**Added** + +* PicDefense as a third external scan provider: POST-based copyright risk analysis via `POST /checkImageRisk`, authenticated with `X-API-TOKEN: {userId}:{apiKey}` header. +* PicDefense results include risk classification (`picrisk`: high/medium/low), backlinks with similarity score and image URL, risk flags (face, logo, landmark, stock, EXIF copyright), and label detection. +* Full Report attachment detail page: PicDefense section with picRisk badge, risk flags list, backlinks table sorted by similarity score (highest first), and detected labels. +* Audit list quick-view modal: picRisk badge shown inline next to PicDefense match count. +* Settings page: PicDefense User ID and API Key fields with credential status indicator. + +**Compatibility** + +* WordPress: 6.8 - 7.0 +* PHP: 8.2 - 8.4 +* WP-CLI: 2.x + +**Tests** + +* PHP Coding Standards: WPCS 3.x / PHPCS 3.x +* PHPStan: level 9 +* PHPCompatibility: PHP 8.2 - 8.4 + += 1.4.0 = + +_Release date: 2026-05-05_ + +**Added** + +* Full attachment detail page accessible via "Full Report" row action in the audit list and "View full report →" link in the quick-view modal. Shows file metadata, internal usage, and complete raw external scan results per provider: + * Google Vision: pages with matching images (URL + page title), full image matches, and partial image matches — all as clickable links. + * TinEye: backlinks table with webpage URL, direct image URL, and crawl date — sorted by crawl date, newest first. + +**Fixed** + +* `rate_limit_per_minute` setting (Settings page) was stored but never read by providers — both Google Vision and TinEye used a hardcoded limit of 10 req/min regardless of configuration. +* `external_batch_size` setting was ignored by Action Scheduler batch processing and by the browser AJAX runner — both always processed 10 attachments per batch. +* TinEye domain extraction used the wrong API field: `backlink.url` is the direct image URL (CDN), while `backlink.backlink` is the webpage URL. The `top_domains` summary and the detail page now show the correct page domains and URLs. + +**Compatibility** + +* WordPress: 6.8 - 7.0 +* PHP: 8.2 - 8.4 +* WP-CLI: 2.x + +**Tests** + +* PHP Coding Standards: WPCS 3.x / PHPCS 3.x +* PHPStan: level 9 +* PHPCompatibility: PHP 8.2 - 8.4 + = 1.3.0 = _Release date: 2026-05-05_ diff --git a/includes/Admin/AttachmentDetailPage.php b/includes/Admin/AttachmentDetailPage.php new file mode 100644 index 0000000..c08b7ec --- /dev/null +++ b/includes/Admin/AttachmentDetailPage.php @@ -0,0 +1,595 @@ +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 '
| %s | %s |
|---|---|
| %s | %s |
| %s | %s |
| %s | %s |
| %s | %s |
| %s | %s |
| %s | %s |
' . $thumb . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_get_attachment_image is safe. + } + + // ----------------------------------------------------------------------- + // Internal Usage + // ----------------------------------------------------------------------- + echo '' . esc_html__( 'Not used in any post.', 'robotstxt-mediaaudit' ) . '
'; + } else { + echo '| ' . esc_html__( 'Post', 'robotstxt-mediaaudit' ) . ' | '; + echo '' . esc_html__( 'Type', 'robotstxt-mediaaudit' ) . ' | '; + echo '' . esc_html__( 'Context', 'robotstxt-mediaaudit' ) . ' | '; + echo '' . esc_html__( 'Status', 'robotstxt-mediaaudit' ) . ' | '; + echo '
|---|---|---|---|
| ' . $post_cell . ' | '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $post_cell already escaped above. + echo '' . esc_html( $post_type ) . ' | '; + echo '' . $ctx_label . ' | '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $ctx_label already escaped above. + echo '' . esc_html( $status ) . ' | '; + echo '
' . esc_html__( 'No external scan data available.', 'robotstxt-mediaaudit' ) . '
'; + return; + } + + $provider_names = array( + 'google_vision' => 'Google Cloud Vision', + 'tineye' => 'TinEye', + 'picdefense' => 'PicDefense', + ); + + 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 : ''; + $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; + } + + echo '| ' . esc_html__( 'Page URL', 'robotstxt-mediaaudit' ) . ' | '; + echo '' . esc_html__( 'Page Title', 'robotstxt-mediaaudit' ) . ' | '; + echo '
|---|---|
| ' . esc_html( $page_url ) . ' | '; + echo '' . esc_html( $page_title ) . ' | '; + echo '
| ' . esc_html__( 'Image URL', 'robotstxt-mediaaudit' ) . ' |
|---|
| ' . esc_html( $img_url ) . ' |
| ' . esc_html__( 'Image URL', 'robotstxt-mediaaudit' ) . ' |
|---|
| ' . esc_html( $img_url ) . ' |
| ' . esc_html__( 'Page URL', 'robotstxt-mediaaudit' ) . ' | '; + echo '' . esc_html__( 'Image URL', 'robotstxt-mediaaudit' ) . ' | '; + echo '' . esc_html__( 'Crawl date', 'robotstxt-mediaaudit' ) . ' | '; + echo '
|---|---|---|
| ' . esc_html( $page_url ) . ' | '; + echo '' . esc_html( $img_url ) . ' | '; + echo '' . esc_html( $bl_date ) . ' | '; + 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__( 'No risk flags detected.', 'robotstxt-mediaaudit' ) . '
'; + } else { + echo '| ' . esc_html__( 'Page URL', 'robotstxt-mediaaudit' ) . ' | '; + echo '' . esc_html__( 'Image URL', 'robotstxt-mediaaudit' ) . ' | '; + echo '' . esc_html__( 'Similarity', 'robotstxt-mediaaudit' ) . ' | '; + echo '
|---|---|---|
| ' . esc_html( $page_url ) . ' | '; + echo '' . esc_html( $img_url ) . ' | '; + echo '' . esc_html( $score ) . ' | '; + echo '
' . esc_html( $label_str ) . '
'; + } + } + } + } +} diff --git a/includes/Admin/AuditPage.php b/includes/Admin/AuditPage.php index b4cb720..36070df 100644 --- a/includes/Admin/AuditPage.php +++ b/includes/Admin/AuditPage.php @@ -7,6 +7,7 @@ namespace MediaRightsAudit\Admin; +use MediaRightsAudit\Admin\AttachmentDetailPage; use MediaRightsAudit\External\ExternalScanner; use MediaRightsAudit\External\ResultsConsolidator; use MediaRightsAudit\Internal\AttachmentIndexer; @@ -269,7 +270,7 @@ class AuditPage { // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared $rows = $wpdb->get_results( $wpdb->prepare( - "SELECT provider, match_count, top_domains, created_at + "SELECT provider, match_count, top_domains, raw_response, created_at FROM {$wpdb->prefix}mra_external_results WHERE attachment_id = %d ORDER BY provider ASC", @@ -285,6 +286,7 @@ class AuditPage { $provider_names = array( 'google_vision' => 'Google Cloud Vision', 'tineye' => 'TinEye', + 'picdefense' => 'PicDefense', ); $html = '' + . esc_html__( 'View full report →', 'robotstxt-mediaaudit' ) + . '
'; + return $html; } diff --git a/includes/Admin/MediaListTable.php b/includes/Admin/MediaListTable.php index 58b3a40..ac4261a 100644 --- a/includes/Admin/MediaListTable.php +++ b/includes/Admin/MediaListTable.php @@ -7,6 +7,8 @@ namespace MediaRightsAudit\Admin; +use MediaRightsAudit\Admin\AttachmentDetailPage; + if ( ! class_exists( 'WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } @@ -172,6 +174,11 @@ class MediaListTable extends \WP_List_Table { $id, __( 'View Details', 'robotstxt-mediaaudit' ) ), + 'full_report' => sprintf( + '%s', + esc_url( AttachmentDetailPage::url( $id ) ), + __( 'Full Report', 'robotstxt-mediaaudit' ) + ), ); return $title . $this->row_actions( $actions ); diff --git a/includes/Admin/Settings.php b/includes/Admin/Settings.php index f141285..5472515 100644 --- a/includes/Admin/Settings.php +++ b/includes/Admin/Settings.php @@ -80,6 +80,22 @@ class Settings { 'mra_api_credentials' ); + add_settings_field( + 'picdefense_user_id', + __( 'PicDefense User ID', 'robotstxt-mediaaudit' ), + array( $this, 'field_picdefense_user_id' ), + 'robotstxt-mediaaudit-settings', + 'mra_api_credentials' + ); + + add_settings_field( + 'picdefense_api_key', + __( 'PicDefense API Key', 'robotstxt-mediaaudit' ), + array( $this, 'field_picdefense_api_key' ), + 'robotstxt-mediaaudit-settings', + 'mra_api_credentials' + ); + add_settings_section( 'mra_external_scanning', __( 'External Scanning', 'robotstxt-mediaaudit' ), @@ -126,6 +142,12 @@ class Settings { $tineye_val = $input['tineye_api_key'] ?? null; $output['tineye_api_key'] = is_string( $tineye_val ) ? sanitize_text_field( $tineye_val ) : ''; + $pd_uid_val = $input['picdefense_user_id'] ?? null; + $output['picdefense_user_id'] = is_string( $pd_uid_val ) ? sanitize_text_field( $pd_uid_val ) : ''; + + $pd_key_val = $input['picdefense_api_key'] ?? null; + $output['picdefense_api_key'] = is_string( $pd_key_val ) ? sanitize_text_field( $pd_key_val ) : ''; + $batch_val = $input['external_batch_size'] ?? null; $batch = is_numeric( $batch_val ) ? (int) $batch_val : 10; $output['external_batch_size'] = max( 1, min( 100, $batch ) ); @@ -265,6 +287,58 @@ class Settings { + ++ +
+ + ++ + — + + + +
+ is_google_vision_configured(); + $te_ok = $this->is_tineye_configured(); + $pd_ok = $this->is_picdefense_configured(); ?>