670 lines
20 KiB
PHP
670 lines
20 KiB
PHP
<?php
|
|
/**
|
|
* External scan batch processor.
|
|
*
|
|
* @package MediaRightsAudit\External
|
|
*/
|
|
|
|
namespace MediaRightsAudit\External;
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
use MediaRightsAudit\Admin\Settings;
|
|
use MediaRightsAudit\Core\Queue\Scheduler;
|
|
|
|
/**
|
|
* Processes the external scan queue: runs each pending attachment through
|
|
* all active providers and stores results in mra_external_results.
|
|
*
|
|
* Per-provider scan state is tracked in mra_provider_status. The aggregate
|
|
* external_status in mra_media_index is updated after each scan for display.
|
|
*
|
|
* Active providers are resolved at runtime via the mra/external/providers filter.
|
|
* Scanning is triggered manually by an administrator (Run External Scan bulk action
|
|
* or wp mra scan-external CLI) — never automatically, to control API costs.
|
|
*
|
|
* Extension hook: mra/external/providers — filter to register additional providers.
|
|
*/
|
|
class ExternalScanner {
|
|
|
|
/**
|
|
* Action Scheduler hook name for background external scanning.
|
|
*/
|
|
const AS_HOOK = 'mra_external_scan_batch';
|
|
|
|
/**
|
|
* Scans one batch of queued attachments with their pending providers.
|
|
*
|
|
* Queries mra_provider_status for queued entries and processes each
|
|
* attachment with the intersection of active providers and queued slugs.
|
|
*
|
|
* @param int $batch_size Maximum number of attachment rows to process.
|
|
*
|
|
* @return int Number of attachment rows attempted.
|
|
*/
|
|
public static function scan_batch( int $batch_size = 10 ): int {
|
|
global $wpdb;
|
|
|
|
$providers = self::get_active_providers();
|
|
if ( empty( $providers ) ) {
|
|
return 0;
|
|
}
|
|
|
|
// Index providers by slug for fast intersection lookup.
|
|
$provider_map = array();
|
|
foreach ( $providers as $provider ) {
|
|
$provider_map[ $provider->provider_slug() ] = $provider;
|
|
}
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
$rows = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT DISTINCT ps.attachment_id, mi.file_url
|
|
FROM {$wpdb->prefix}mra_provider_status ps
|
|
JOIN {$wpdb->prefix}mra_media_index mi ON mi.attachment_id = ps.attachment_id
|
|
WHERE ps.status = 'queued'
|
|
ORDER BY ps.attachment_id ASC
|
|
LIMIT %d",
|
|
$batch_size
|
|
),
|
|
ARRAY_A
|
|
);
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
|
|
if ( ! $rows ) {
|
|
return 0;
|
|
}
|
|
|
|
foreach ( $rows as $row ) {
|
|
$aid_val = $row['attachment_id'] ?? null;
|
|
$url_val = $row['file_url'] ?? null;
|
|
$aid = is_numeric( $aid_val ) ? (int) $aid_val : 0;
|
|
$url = is_string( $url_val ) ? $url_val : '';
|
|
|
|
if ( $aid <= 0 || '' === $url ) {
|
|
continue;
|
|
}
|
|
|
|
// Get queued provider slugs for this attachment.
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
$queued_slugs = $wpdb->get_col(
|
|
$wpdb->prepare(
|
|
"SELECT provider FROM {$wpdb->prefix}mra_provider_status
|
|
WHERE attachment_id = %d AND status = 'queued'",
|
|
$aid
|
|
)
|
|
);
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
|
|
if ( empty( $queued_slugs ) ) {
|
|
continue;
|
|
}
|
|
|
|
// Intersect queued slugs with currently active providers.
|
|
$batch_providers = array();
|
|
foreach ( $queued_slugs as $slug ) {
|
|
if ( is_string( $slug ) && isset( $provider_map[ $slug ] ) ) {
|
|
$batch_providers[] = $provider_map[ $slug ];
|
|
}
|
|
}
|
|
|
|
if ( empty( $batch_providers ) ) {
|
|
continue;
|
|
}
|
|
|
|
self::process_attachment( $aid, $url, $batch_providers );
|
|
}
|
|
|
|
return count( $rows );
|
|
}
|
|
|
|
/**
|
|
* Returns the count of provider-attachment rows still waiting for external scanning.
|
|
*
|
|
* @return int
|
|
*/
|
|
public static function get_pending_count(): int {
|
|
global $wpdb;
|
|
|
|
$result = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
"SELECT COUNT(*) FROM {$wpdb->prefix}mra_provider_status WHERE status = 'queued'"
|
|
);
|
|
|
|
return is_numeric( $result ) ? (int) $result : 0;
|
|
}
|
|
|
|
/**
|
|
* Processes one scheduled batch and re-queues if more remain.
|
|
*
|
|
* Called by Action Scheduler via the mra_external_scan_batch hook.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function process_scheduled_batch(): void {
|
|
$opts = Settings::get_effective_settings();
|
|
$bs_val = $opts['external_batch_size'] ?? null;
|
|
$batch_size = is_numeric( $bs_val ) ? max( 1, (int) $bs_val ) : 10;
|
|
|
|
self::scan_batch( $batch_size );
|
|
|
|
delete_transient( 'mra_alert_count' );
|
|
|
|
if ( is_multisite() && 'central' === get_site_option( 'robotstxt_mediaaudit_network_mode', 'per_site' ) ) {
|
|
\MediaRightsAudit\Core\NetworkDatabase::sync_site( get_current_blog_id() );
|
|
}
|
|
|
|
if ( self::get_pending_count() > 0 ) {
|
|
Scheduler::schedule_single( self::AS_HOOK );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Marks all pending provider rows as queued and schedules a background scan batch.
|
|
*
|
|
* Calls queue_provider_attachments() for each active provider to populate
|
|
* mra_provider_status, then syncs the aggregate status on mra_media_index.
|
|
* Only schedules the Action Scheduler action if there are queued items and
|
|
* no batch is already running.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function schedule(): void {
|
|
$providers = self::get_active_providers();
|
|
foreach ( $providers as $provider ) {
|
|
self::queue_provider_attachments( $provider->provider_slug() );
|
|
}
|
|
|
|
self::sync_aggregate_queued_status();
|
|
|
|
if ( self::get_pending_count() > 0 && ! Scheduler::has_pending( self::AS_HOOK ) ) {
|
|
Scheduler::schedule_single( self::AS_HOOK );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Promotes all pending provider rows to queued for all active providers.
|
|
*
|
|
* Does not schedule an Action Scheduler action. Useful in CLI context where
|
|
* processing runs synchronously in a loop rather than via background jobs.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function queue_all_pending(): void {
|
|
$providers = self::get_active_providers();
|
|
foreach ( $providers as $provider ) {
|
|
self::queue_provider_attachments( $provider->provider_slug() );
|
|
}
|
|
|
|
self::sync_aggregate_queued_status();
|
|
}
|
|
|
|
/**
|
|
* Creates scan entries for newly registered providers.
|
|
*
|
|
* For each active provider, inserts mra_provider_status rows for attachments
|
|
* that have no existing row for that provider. Already-scanned or error rows
|
|
* are not touched. If new rows are created, syncs aggregate status and
|
|
* schedules a background scan batch.
|
|
*
|
|
* @return int Total number of new rows inserted.
|
|
*/
|
|
public static function sync_providers(): int {
|
|
global $wpdb;
|
|
|
|
$providers = self::get_active_providers();
|
|
$total = 0;
|
|
|
|
foreach ( $providers as $provider ) {
|
|
$slug = $provider->provider_slug();
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"INSERT IGNORE INTO {$wpdb->prefix}mra_provider_status (attachment_id, provider, status)
|
|
SELECT mi.attachment_id, %s, 'queued'
|
|
FROM {$wpdb->prefix}mra_media_index mi
|
|
LEFT JOIN {$wpdb->prefix}mra_provider_status ps
|
|
ON ps.attachment_id = mi.attachment_id AND ps.provider = %s
|
|
WHERE ps.attachment_id IS NULL",
|
|
$slug,
|
|
$slug
|
|
)
|
|
);
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
|
|
$total += (int) $wpdb->rows_affected;
|
|
}
|
|
|
|
if ( $total > 0 ) {
|
|
self::sync_aggregate_queued_status();
|
|
|
|
if ( ! Scheduler::has_pending( self::AS_HOOK ) ) {
|
|
Scheduler::schedule_single( self::AS_HOOK );
|
|
}
|
|
}
|
|
|
|
return $total;
|
|
}
|
|
|
|
/**
|
|
* Marks specific attachments as queued for all active providers and schedules a scan batch.
|
|
*
|
|
* Idempotent: already-queued or scanned rows are left unchanged;
|
|
* error rows are reset to queued.
|
|
*
|
|
* @param array<int, int> $attachment_ids Attachment IDs to queue. Must be non-empty.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function queue_attachments( array $attachment_ids ): void {
|
|
if ( empty( $attachment_ids ) ) {
|
|
return;
|
|
}
|
|
|
|
global $wpdb;
|
|
|
|
$ids = array_map( 'intval', $attachment_ids );
|
|
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
|
|
$providers = self::get_active_providers();
|
|
|
|
foreach ( $providers as $provider ) {
|
|
$slug = $provider->provider_slug();
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"INSERT INTO {$wpdb->prefix}mra_provider_status (attachment_id, provider, status)
|
|
SELECT attachment_id, %s, 'queued'
|
|
FROM {$wpdb->prefix}mra_media_index
|
|
WHERE attachment_id IN ({$placeholders})
|
|
ON DUPLICATE KEY UPDATE status = IF(status NOT IN ('scanned','queued'),'queued',status)",
|
|
array_merge( array( $slug ), $ids )
|
|
)
|
|
);
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
|
}
|
|
|
|
self::sync_aggregate_queued_status();
|
|
|
|
if ( ! Scheduler::has_pending( self::AS_HOOK ) ) {
|
|
Scheduler::schedule_single( self::AS_HOOK );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Re-queues all provider rows that previously failed with status = 'error'.
|
|
*
|
|
* Does not touch pending, queued, or scanned rows.
|
|
* Schedules a background scan batch if any items were re-queued and
|
|
* no batch is already pending.
|
|
*
|
|
* @return int Number of provider rows re-queued.
|
|
*/
|
|
public static function requeue_errors(): int {
|
|
global $wpdb;
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery
|
|
$count = (int) $wpdb->get_var(
|
|
"SELECT COUNT(*) FROM {$wpdb->prefix}mra_provider_status WHERE status = 'error'"
|
|
);
|
|
|
|
if ( $count > 0 ) {
|
|
$wpdb->query(
|
|
"UPDATE {$wpdb->prefix}mra_provider_status
|
|
SET status = 'queued', scanned_at = NULL
|
|
WHERE status = 'error'"
|
|
);
|
|
|
|
self::sync_aggregate_queued_status();
|
|
|
|
if ( ! Scheduler::has_pending( self::AS_HOOK ) ) {
|
|
Scheduler::schedule_single( self::AS_HOOK );
|
|
}
|
|
}
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* Deletes external scan results and resets status to pending for given IDs.
|
|
*
|
|
* @param array<int, int> $attachment_ids Attachment IDs to purge.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function purge( array $attachment_ids ): void {
|
|
if ( empty( $attachment_ids ) ) {
|
|
return;
|
|
}
|
|
|
|
global $wpdb;
|
|
|
|
$ids = array_map( 'intval', $attachment_ids );
|
|
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"DELETE FROM {$wpdb->prefix}mra_external_results
|
|
WHERE attachment_id IN ({$placeholders})",
|
|
...$ids
|
|
)
|
|
);
|
|
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"DELETE FROM {$wpdb->prefix}mra_provider_status
|
|
WHERE attachment_id IN ({$placeholders})",
|
|
...$ids
|
|
)
|
|
);
|
|
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"UPDATE {$wpdb->prefix}mra_media_index
|
|
SET external_status = 'pending', external_scanned_at = NULL
|
|
WHERE attachment_id IN ({$placeholders})",
|
|
...$ids
|
|
)
|
|
);
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
|
|
|
delete_transient( 'mra_alert_count' );
|
|
}
|
|
|
|
/**
|
|
* Deletes all external scan results and resets every attachment's external_status to pending.
|
|
*
|
|
* @return int Number of result rows deleted from mra_external_results.
|
|
*/
|
|
public static function purge_all(): int {
|
|
global $wpdb;
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery
|
|
$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}mra_external_results" );
|
|
$wpdb->query( "DELETE FROM {$wpdb->prefix}mra_external_results" );
|
|
$wpdb->query( "DELETE FROM {$wpdb->prefix}mra_provider_status" );
|
|
$wpdb->query(
|
|
"UPDATE {$wpdb->prefix}mra_media_index
|
|
SET external_status = 'pending', external_scanned_at = NULL"
|
|
);
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery
|
|
|
|
delete_transient( 'mra_alert_count' );
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* Returns a count of attachments grouped by external_status.
|
|
*
|
|
* Reads from the mra_media_index aggregate column for display purposes.
|
|
*
|
|
* @return array<string, int>
|
|
*/
|
|
public static function get_status_counts(): array {
|
|
global $wpdb;
|
|
|
|
$rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
"SELECT external_status, COUNT(*) AS cnt
|
|
FROM {$wpdb->prefix}mra_media_index
|
|
GROUP BY external_status",
|
|
ARRAY_A
|
|
);
|
|
|
|
$counts = array(
|
|
'pending' => 0,
|
|
'queued' => 0,
|
|
'scanned' => 0,
|
|
'matches' => 0,
|
|
'error' => 0,
|
|
);
|
|
|
|
if ( is_array( $rows ) ) {
|
|
foreach ( $rows as $row ) {
|
|
$s = is_array( $row ) && is_string( $row['external_status'] ) ? $row['external_status'] : '';
|
|
if ( isset( $counts[ $s ] ) ) {
|
|
$cnt = isset( $row['cnt'] ) && is_numeric( $row['cnt'] ) ? (int) $row['cnt'] : 0;
|
|
$counts[ $s ] = $cnt;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $counts;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Private helpers
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Scans a single attachment with specified providers and updates per-provider status.
|
|
*
|
|
* After scanning all providers, calls update_aggregate_status() to sync
|
|
* the mra_media_index aggregate column.
|
|
*
|
|
* @param int $attachment_id WordPress attachment ID.
|
|
* @param string $file_url Public URL of the image.
|
|
* @param array<AbstractProvider> $providers Resolved provider list for this attachment.
|
|
*
|
|
* @return void
|
|
*/
|
|
private static function process_attachment( int $attachment_id, string $file_url, array $providers ): void {
|
|
global $wpdb;
|
|
|
|
foreach ( $providers as $provider ) {
|
|
$slug = $provider->provider_slug();
|
|
|
|
try {
|
|
$result = $provider->scan( $attachment_id, $file_url );
|
|
self::save_result( $attachment_id, $slug, $result );
|
|
|
|
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->prepare(
|
|
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
"UPDATE {$wpdb->prefix}mra_provider_status
|
|
SET status = 'scanned', scanned_at = NOW()
|
|
WHERE attachment_id = %d AND provider = %s",
|
|
$attachment_id,
|
|
$slug
|
|
)
|
|
);
|
|
} catch ( \RuntimeException $e ) {
|
|
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->prepare(
|
|
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
"UPDATE {$wpdb->prefix}mra_provider_status
|
|
SET status = 'error', scanned_at = NOW()
|
|
WHERE attachment_id = %d AND provider = %s",
|
|
$attachment_id,
|
|
$slug
|
|
)
|
|
);
|
|
|
|
if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
|
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
|
error_log(
|
|
sprintf(
|
|
'[MRA] %s scan failed for attachment %d: %s',
|
|
$slug,
|
|
$attachment_id,
|
|
$e->getMessage()
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
self::update_aggregate_status( $attachment_id );
|
|
}
|
|
|
|
/**
|
|
* Recomputes and persists the aggregate external_status for a single attachment.
|
|
*
|
|
* Logic:
|
|
* - Any provider still queued → 'queued'
|
|
* - No provider succeeded → 'error'
|
|
* - At least one match found → 'matches'
|
|
* - Otherwise → 'scanned'
|
|
*
|
|
* @param int $attachment_id WordPress attachment ID.
|
|
*
|
|
* @return void
|
|
*/
|
|
private static function update_aggregate_status( int $attachment_id ): void {
|
|
global $wpdb;
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
$row = $wpdb->get_row(
|
|
$wpdb->prepare(
|
|
"SELECT
|
|
SUM(status = 'queued') AS queued_count,
|
|
SUM(status = 'scanned') AS scanned_count
|
|
FROM {$wpdb->prefix}mra_provider_status
|
|
WHERE attachment_id = %d",
|
|
$attachment_id
|
|
),
|
|
ARRAY_A
|
|
);
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
|
|
if ( ! is_array( $row ) ) {
|
|
return;
|
|
}
|
|
|
|
$queued_count = isset( $row['queued_count'] ) && is_numeric( $row['queued_count'] ) ? (int) $row['queued_count'] : 0;
|
|
$scanned_count = isset( $row['scanned_count'] ) && is_numeric( $row['scanned_count'] ) ? (int) $row['scanned_count'] : 0;
|
|
|
|
if ( $queued_count > 0 ) {
|
|
$new_status = 'queued';
|
|
} elseif ( 0 === $scanned_count ) {
|
|
$new_status = 'error';
|
|
} else {
|
|
// Check for matches across scanned providers.
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
$match_sum = $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
"SELECT SUM(match_count) FROM {$wpdb->prefix}mra_external_results WHERE attachment_id = %d",
|
|
$attachment_id
|
|
)
|
|
);
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
|
|
$new_status = ( is_numeric( $match_sum ) && (int) $match_sum > 0 ) ? 'matches' : 'scanned';
|
|
}
|
|
|
|
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->prefix . 'mra_media_index',
|
|
array(
|
|
'external_status' => $new_status,
|
|
'external_scanned_at' => current_time( 'mysql', true ),
|
|
),
|
|
array( 'attachment_id' => $attachment_id ),
|
|
array( '%s', '%s' ),
|
|
array( '%d' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Updates mra_media_index.external_status to 'queued' for all attachments
|
|
* that have at least one queued row in mra_provider_status.
|
|
*
|
|
* Only updates rows not already marked 'queued', to avoid unnecessary writes.
|
|
*
|
|
* @return void
|
|
*/
|
|
private static function sync_aggregate_queued_status(): void {
|
|
global $wpdb;
|
|
|
|
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
"UPDATE {$wpdb->prefix}mra_media_index mi
|
|
INNER JOIN (
|
|
SELECT DISTINCT attachment_id
|
|
FROM {$wpdb->prefix}mra_provider_status
|
|
WHERE status = 'queued'
|
|
) AS q ON q.attachment_id = mi.attachment_id
|
|
SET mi.external_status = 'queued'
|
|
WHERE mi.external_status != 'queued'"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Inserts or updates mra_provider_status rows for all indexed attachments.
|
|
*
|
|
* For attachments without an existing row for this provider, inserts 'queued'.
|
|
* For attachments with an existing non-scanned, non-queued row, resets to 'queued'.
|
|
* Scanned and already-queued rows are left unchanged.
|
|
*
|
|
* @param string $provider_slug Provider machine-readable slug.
|
|
*
|
|
* @return void
|
|
*/
|
|
private static function queue_provider_attachments( string $provider_slug ): void {
|
|
global $wpdb;
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"INSERT INTO {$wpdb->prefix}mra_provider_status (attachment_id, provider, status)
|
|
SELECT attachment_id, %s, 'queued'
|
|
FROM {$wpdb->prefix}mra_media_index
|
|
ON DUPLICATE KEY UPDATE status = IF(status NOT IN ('scanned','queued'),'queued',status)",
|
|
$provider_slug
|
|
)
|
|
);
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
}
|
|
|
|
/**
|
|
* Persists a provider's scan result (upsert via REPLACE INTO).
|
|
*
|
|
* @param int $attachment_id WordPress attachment ID.
|
|
* @param string $provider_slug Provider machine-readable slug.
|
|
* @param ScanResult $result Scan result to store.
|
|
*
|
|
* @return void
|
|
*/
|
|
private static function save_result( int $attachment_id, string $provider_slug, ScanResult $result ): void {
|
|
global $wpdb;
|
|
|
|
$raw_json = wp_json_encode( $result->raw_response() );
|
|
$domains_json = wp_json_encode( $result->top_domains() );
|
|
|
|
$wpdb->replace( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->prefix . 'mra_external_results',
|
|
array(
|
|
'attachment_id' => $attachment_id,
|
|
'provider' => $provider_slug,
|
|
'raw_response' => ( false !== $raw_json ) ? $raw_json : '{}',
|
|
'match_count' => $result->match_count(),
|
|
'top_domains' => ( false !== $domains_json ) ? $domains_json : '{}',
|
|
'created_at' => current_time( 'mysql', true ),
|
|
),
|
|
array( '%d', '%s', '%s', '%d', '%s', '%s' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resolves active providers from the mra/external/providers filter.
|
|
*
|
|
* @return array<AbstractProvider>
|
|
*/
|
|
private static function get_active_providers(): array {
|
|
$raw = apply_filters( 'mra/external/providers', array() ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
|
|
$providers = array();
|
|
|
|
if ( is_array( $raw ) ) {
|
|
foreach ( $raw as $p ) {
|
|
if ( $p instanceof AbstractProvider ) {
|
|
$providers[] = $p;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $providers;
|
|
}
|
|
}
|